Run prettier on all files.

This commit is contained in:
Paul
2021-07-05 11:23:23 +01:00
parent 56058c1e75
commit 7bd33d8d34
181 changed files with 18084 additions and 13521 deletions

View File

@@ -1,22 +1,23 @@
import { useContext } from "preact/hooks";
import { Redirect } from "react-router-dom";
import { Children } from "../../types/Preact";
import { useContext } from "preact/hooks";
import { Children } from "../../types/Preact";
import { OperationsContext } from "./RevoltClient";
interface Props {
auth?: boolean;
children: Children;
auth?: boolean;
children: Children;
}
export const CheckAuth = (props: Props) => {
const operations = useContext(OperationsContext);
const operations = useContext(OperationsContext);
if (props.auth && !operations.ready()) {
return <Redirect to="/login" />;
} else if (!props.auth && operations.ready()) {
return <Redirect to="/" />;
}
if (props.auth && !operations.ready()) {
return <Redirect to="/login" />;
} else if (!props.auth && operations.ready()) {
return <Redirect to="/" />;
}
return <>{props.children}</>;
return <>{props.children}</>;
};

View File

@@ -1,221 +1,292 @@
import { Text } from "preact-i18n";
import { takeError } from "./util";
import classNames from "classnames";
import { AppContext } from "./RevoltClient";
import styles from './FileUploads.module.scss';
import Axios, { AxiosRequestConfig } from "axios";
import { useContext, useEffect, useState } from "preact/hooks";
import Preloader from "../../components/ui/Preloader";
import { determineFileSize } from "../../lib/fileSize";
import IconButton from '../../components/ui/IconButton';
import { Plus, X, XCircle } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid";
import Axios, { AxiosRequestConfig } from "axios";
import styles from "./FileUploads.module.scss";
import classNames from "classnames";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { determineFileSize } from "../../lib/fileSize";
import IconButton from "../../components/ui/IconButton";
import Preloader from "../../components/ui/Preloader";
import { useIntermediate } from "../intermediate/Intermediate";
import { AppContext } from "./RevoltClient";
import { takeError } from "./util";
type Props = {
maxFileSize: number
remove: () => Promise<void>
fileType: 'backgrounds' | 'icons' | 'avatars' | 'attachments' | 'banners'
maxFileSize: number;
remove: () => Promise<void>;
fileType: "backgrounds" | "icons" | "avatars" | "attachments" | "banners";
} & (
{ behaviour: 'ask', onChange: (file: File) => void } |
{ behaviour: 'upload', onUpload: (id: string) => Promise<void> } |
{ behaviour: 'multi', onChange: (files: File[]) => void, append?: (files: File[]) => void }
) & (
{ style: 'icon' | 'banner', defaultPreview?: string, previewURL?: string, width?: number, height?: number } |
{ style: 'attachment', attached: boolean, uploading: boolean, cancel: () => void, size?: number }
)
| { behaviour: "ask"; onChange: (file: File) => void }
| { behaviour: "upload"; onUpload: (id: string) => Promise<void> }
| {
behaviour: "multi";
onChange: (files: File[]) => void;
append?: (files: File[]) => void;
}
) &
(
| {
style: "icon" | "banner";
defaultPreview?: string;
previewURL?: string;
width?: number;
height?: number;
}
| {
style: "attachment";
attached: boolean;
uploading: boolean;
cancel: () => void;
size?: number;
}
);
export async function uploadFile(autumnURL: string, tag: string, file: File, config?: AxiosRequestConfig) {
const formData = new FormData();
formData.append("file", file);
const res = await Axios.post(autumnURL + "/" + tag, formData, {
headers: {
"Content-Type": "multipart/form-data"
},
...config
});
export async function uploadFile(
autumnURL: string,
tag: string,
file: File,
config?: AxiosRequestConfig,
) {
const formData = new FormData();
formData.append("file", file);
return res.data.id;
const res = await Axios.post(autumnURL + "/" + tag, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
...config,
});
return res.data.id;
}
export function grabFiles(maxFileSize: number, cb: (files: File[]) => void, tooLarge: () => void, multiple?: boolean) {
const input = document.createElement("input");
input.type = "file";
input.multiple = multiple ?? false;
export function grabFiles(
maxFileSize: number,
cb: (files: File[]) => void,
tooLarge: () => void,
multiple?: boolean,
) {
const input = document.createElement("input");
input.type = "file";
input.multiple = multiple ?? false;
input.onchange = async (e) => {
const files = (e.currentTarget as HTMLInputElement)?.files;
if (!files) return;
for (let file of files) {
if (file.size > maxFileSize) {
return tooLarge();
}
}
input.onchange = async (e) => {
const files = (e.currentTarget as HTMLInputElement)?.files;
if (!files) return;
for (let file of files) {
if (file.size > maxFileSize) {
return tooLarge();
}
}
cb(Array.from(files));
};
cb(Array.from(files));
};
input.click();
input.click();
}
export function FileUploader(props: Props) {
const { fileType, maxFileSize, remove } = props;
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const { fileType, maxFileSize, remove } = props;
const { openScreen } = useIntermediate();
const client = useContext(AppContext);
const [ uploading, setUploading ] = useState(false);
const [uploading, setUploading] = useState(false);
function onClick() {
if (uploading) return;
function onClick() {
if (uploading) return;
grabFiles(maxFileSize, async files => {
setUploading(true);
grabFiles(
maxFileSize,
async (files) => {
setUploading(true);
try {
if (props.behaviour === 'multi') {
props.onChange(files);
} else if (props.behaviour === 'ask') {
props.onChange(files[0]);
} else {
await props.onUpload(await uploadFile(client.configuration!.features.autumn.url, fileType, files[0]));
}
} catch (err) {
return openScreen({ id: "error", error: takeError(err) });
} finally {
setUploading(false);
}
}, () =>
openScreen({ id: "error", error: "FileTooLarge" }),
props.behaviour === 'multi');
}
try {
if (props.behaviour === "multi") {
props.onChange(files);
} else if (props.behaviour === "ask") {
props.onChange(files[0]);
} else {
await props.onUpload(
await uploadFile(
client.configuration!.features.autumn.url,
fileType,
files[0],
),
);
}
} catch (err) {
return openScreen({ id: "error", error: takeError(err) });
} finally {
setUploading(false);
}
},
() => openScreen({ id: "error", error: "FileTooLarge" }),
props.behaviour === "multi",
);
}
function removeOrUpload() {
if (uploading) return;
function removeOrUpload() {
if (uploading) return;
if (props.style === 'attachment') {
if (props.attached) {
props.remove();
} else {
onClick();
}
} else {
if (props.previewURL) {
props.remove();
} else {
onClick();
}
}
}
if (props.style === "attachment") {
if (props.attached) {
props.remove();
} else {
onClick();
}
} else {
if (props.previewURL) {
props.remove();
} else {
onClick();
}
}
}
if (props.behaviour === 'multi' && props.append) {
useEffect(() => {
// File pasting.
function paste(e: ClipboardEvent) {
const items = e.clipboardData?.items;
if (typeof items === "undefined") return;
if (props.behaviour !== 'multi' || !props.append) return;
if (props.behaviour === "multi" && props.append) {
useEffect(() => {
// File pasting.
function paste(e: ClipboardEvent) {
const items = e.clipboardData?.items;
if (typeof items === "undefined") return;
if (props.behaviour !== "multi" || !props.append) return;
let files = [];
for (const item of items) {
if (!item.type.startsWith("text/")) {
const blob = item.getAsFile();
if (blob) {
if (blob.size > props.maxFileSize) {
openScreen({ id: 'error', error: 'FileTooLarge' });
}
let files = [];
for (const item of items) {
if (!item.type.startsWith("text/")) {
const blob = item.getAsFile();
if (blob) {
if (blob.size > props.maxFileSize) {
openScreen({
id: "error",
error: "FileTooLarge",
});
}
files.push(blob);
}
}
}
files.push(blob);
}
}
}
props.append(files);
}
props.append(files);
}
// Let the browser know we can drop files.
function dragover(e: DragEvent) {
e.stopPropagation();
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
}
// Let the browser know we can drop files.
function dragover(e: DragEvent) {
e.stopPropagation();
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
}
// File dropping.
function drop(e: DragEvent) {
e.preventDefault();
if (props.behaviour !== 'multi' || !props.append) return;
// File dropping.
function drop(e: DragEvent) {
e.preventDefault();
if (props.behaviour !== "multi" || !props.append) return;
const dropped = e.dataTransfer?.files;
if (dropped) {
let files = [];
for (const item of dropped) {
if (item.size > props.maxFileSize) {
openScreen({ id: 'error', error: 'FileTooLarge' });
}
const dropped = e.dataTransfer?.files;
if (dropped) {
let files = [];
for (const item of dropped) {
if (item.size > props.maxFileSize) {
openScreen({ id: "error", error: "FileTooLarge" });
}
files.push(item);
}
files.push(item);
}
props.append(files);
}
}
props.append(files);
}
}
document.addEventListener("paste", paste);
document.addEventListener("dragover", dragover);
document.addEventListener("drop", drop);
document.addEventListener("paste", paste);
document.addEventListener("dragover", dragover);
document.addEventListener("drop", drop);
return () => {
document.removeEventListener("paste", paste);
document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop);
};
}, [ props.append ]);
}
return () => {
document.removeEventListener("paste", paste);
document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop);
};
}, [props.append]);
}
if (props.style === 'icon' || props.style === 'banner') {
const { style, previewURL, defaultPreview, width, height } = props;
return (
<div className={classNames(styles.uploader,
{ [styles.icon]: style === 'icon',
[styles.banner]: style === 'banner' })}
data-uploading={uploading}>
<div className={styles.image}
style={{ backgroundImage:
style === 'icon' ? `url('${previewURL ?? defaultPreview}')` :
(previewURL ? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')` : 'black'),
width, height
}}
onClick={onClick}>
{ uploading ?
<div className={styles.uploading}>
<Preloader type="ring" />
</div> :
<div className={styles.edit}>
<Pencil size={30} />
</div> }
</div>
<div className={styles.modify}>
<span onClick={removeOrUpload}>{
uploading ? <Text id="app.main.channel.uploading_file" /> :
props.previewURL ? <Text id="app.settings.actions.remove" /> :
<Text id="app.settings.actions.upload" /> }</span>
<span className={styles.small}><Text id="app.settings.actions.max_filesize" fields={{ filesize: determineFileSize(maxFileSize) }} /></span>
</div>
</div>
)
} else if (props.style === 'attachment') {
const { attached, uploading, cancel, size } = props;
return (
<IconButton
onClick={() => {
if (uploading) return cancel();
if (attached) return remove();
onClick();
}}>
{ uploading ? <XCircle size={size} /> : attached ? <X size={size} /> : <Plus size={size} />}
</IconButton>
)
}
if (props.style === "icon" || props.style === "banner") {
const { style, previewURL, defaultPreview, width, height } = props;
return (
<div
className={classNames(styles.uploader, {
[styles.icon]: style === "icon",
[styles.banner]: style === "banner",
})}
data-uploading={uploading}>
<div
className={styles.image}
style={{
backgroundImage:
style === "icon"
? `url('${previewURL ?? defaultPreview}')`
: previewURL
? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')`
: "black",
width,
height,
}}
onClick={onClick}>
{uploading ? (
<div className={styles.uploading}>
<Preloader type="ring" />
</div>
) : (
<div className={styles.edit}>
<Pencil size={30} />
</div>
)}
</div>
<div className={styles.modify}>
<span onClick={removeOrUpload}>
{uploading ? (
<Text id="app.main.channel.uploading_file" />
) : props.previewURL ? (
<Text id="app.settings.actions.remove" />
) : (
<Text id="app.settings.actions.upload" />
)}
</span>
<span className={styles.small}>
<Text
id="app.settings.actions.max_filesize"
fields={{
filesize: determineFileSize(maxFileSize),
}}
/>
</span>
</div>
</div>
);
} else if (props.style === "attachment") {
const { attached, uploading, cancel, size } = props;
return (
<IconButton
onClick={() => {
if (uploading) return cancel();
if (attached) return remove();
onClick();
}}>
{uploading ? (
<XCircle size={size} />
) : attached ? (
<X size={size} />
) : (
<Plus size={size} />
)}
</IconButton>
);
}
return null;
return null;
}

View File

@@ -1,247 +1,286 @@
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
import { Users } from "revolt.js/dist/api/objects";
import { decodeTime } from "ulid";
import { useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n";
import { connectState } from "../../redux/connector";
import {
getNotificationState,
Notifications,
shouldNotify,
} from "../../redux/reducers/notifications";
import { NotificationOptions } from "../../redux/reducers/settings";
import { SoundContext } from "../Settings";
import { AppContext } from "./RevoltClient";
import { useTranslation } from "../../lib/i18n";
import { Users } from "revolt.js/dist/api/objects";
import { useContext, useEffect } from "preact/hooks";
import { connectState } from "../../redux/connector";
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
import { NotificationOptions } from "../../redux/reducers/settings";
import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { getNotificationState, Notifications, shouldNotify } from "../../redux/reducers/notifications";
interface Props {
options?: NotificationOptions;
notifs: Notifications;
options?: NotificationOptions;
notifs: Notifications;
}
const notifications: { [key: string]: Notification } = {};
async function createNotification(title: string, options: globalThis.NotificationOptions) {
try {
return new Notification(title, options);
} catch (err) {
let sw = await navigator.serviceWorker.getRegistration();
sw?.showNotification(title, options);
}
async function createNotification(
title: string,
options: globalThis.NotificationOptions,
) {
try {
return new Notification(title, options);
} catch (err) {
let sw = await navigator.serviceWorker.getRegistration();
sw?.showNotification(title, options);
}
}
function Notifier({ options, notifs }: Props) {
const translate = useTranslation();
const showNotification = options?.desktopEnabled ?? false;
const translate = useTranslation();
const showNotification = options?.desktopEnabled ?? false;
const client = useContext(AppContext);
const { guild: guild_id, channel: channel_id } = useParams<{
guild: string;
channel: string;
}>();
const history = useHistory();
const playSound = useContext(SoundContext);
const client = useContext(AppContext);
const { guild: guild_id, channel: channel_id } = useParams<{
guild: string;
channel: string;
}>();
const history = useHistory();
const playSound = useContext(SoundContext);
async function message(msg: Message) {
if (msg.author === client.user!._id) return;
if (msg.channel === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Users.Presence.Busy) return;
async function message(msg: Message) {
if (msg.author === client.user!._id) return;
if (msg.channel === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Users.Presence.Busy) return;
const channel = client.channels.get(msg.channel);
const author = client.users.get(msg.author);
if (!channel) return;
if (author?.relationship === Users.Relationship.Blocked) return;
const channel = client.channels.get(msg.channel);
const author = client.users.get(msg.author);
if (!channel) return;
if (author?.relationship === Users.Relationship.Blocked) return;
const notifState = getNotificationState(notifs, channel);
if (!shouldNotify(notifState, msg, client.user!._id)) return;
const notifState = getNotificationState(notifs, channel);
if (!shouldNotify(notifState, msg, client.user!._id)) return;
playSound('message');
if (!showNotification) return;
playSound("message");
if (!showNotification) return;
let title;
switch (channel.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${author?.username}`;
break;
case "Group":
if (author?._id === SYSTEM_USER_ID) {
title = channel.name;
} else {
title = `@${author?.username} - ${channel.name}`;
}
break;
case "TextChannel":
const server = client.servers.get(channel.server);
title = `@${author?.username} (#${channel.name}, ${server?.name})`;
break;
default:
title = msg.channel;
break;
}
let title;
switch (channel.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${author?.username}`;
break;
case "Group":
if (author?._id === SYSTEM_USER_ID) {
title = channel.name;
} else {
title = `@${author?.username} - ${channel.name}`;
}
break;
case "TextChannel":
const server = client.servers.get(channel.server);
title = `@${author?.username} (#${channel.name}, ${server?.name})`;
break;
default:
title = msg.channel;
break;
}
let image;
if (msg.attachments) {
let imageAttachment = msg.attachments.find(x => x.metadata.type === 'Image');
if (imageAttachment) {
image = client.generateFileURL(imageAttachment, { max_side: 720 });
}
}
let image;
if (msg.attachments) {
let imageAttachment = msg.attachments.find(
(x) => x.metadata.type === "Image",
);
if (imageAttachment) {
image = client.generateFileURL(imageAttachment, {
max_side: 720,
});
}
}
let body, icon;
if (typeof msg.content === "string") {
body = client.markdownToText(msg.content);
icon = client.users.getAvatarURL(msg.author, { max_side: 256 });
} else {
let users = client.users;
switch (msg.content.type) {
case "user_added":
case "user_remove":
body = translate(
`app.main.channel.system.${msg.content.type === 'user_added' ? 'added_by' : 'removed_by'}`,
{ user: users.get(msg.content.id)?.username, other_user: users.get(msg.content.by)?.username }
);
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.id)?.username }
);
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
break;
case "channel_renamed":
body = translate(
`app.main.channel.system.channel_renamed`,
{ user: users.get(msg.content.by)?.username, name: msg.content.name }
);
icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
break;
case "channel_description_changed":
case "channel_icon_changed":
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.by)?.username }
);
icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
break;
}
}
let body, icon;
if (typeof msg.content === "string") {
body = client.markdownToText(msg.content);
icon = client.users.getAvatarURL(msg.author, { max_side: 256 });
} else {
let users = client.users;
switch (msg.content.type) {
case "user_added":
case "user_remove":
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: users.get(msg.content.id)?.username,
other_user: users.get(msg.content.by)?.username,
},
);
icon = client.users.getAvatarURL(msg.content.id, {
max_side: 256,
});
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.id)?.username },
);
icon = client.users.getAvatarURL(msg.content.id, {
max_side: 256,
});
break;
case "channel_renamed":
body = translate(
`app.main.channel.system.channel_renamed`,
{
user: users.get(msg.content.by)?.username,
name: msg.content.name,
},
);
icon = client.users.getAvatarURL(msg.content.by, {
max_side: 256,
});
break;
case "channel_description_changed":
case "channel_icon_changed":
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.by)?.username },
);
icon = client.users.getAvatarURL(msg.content.by, {
max_side: 256,
});
break;
}
}
let notif = await createNotification(title, {
icon,
image,
body,
timestamp: decodeTime(msg._id),
tag: msg.channel,
badge: '/assets/icons/android-chrome-512x512.png',
silent: true
});
let notif = await createNotification(title, {
icon,
image,
body,
timestamp: decodeTime(msg._id),
tag: msg.channel,
badge: "/assets/icons/android-chrome-512x512.png",
silent: true,
});
if (notif) {
notif.addEventListener("click", () => {
window.focus();
const id = msg.channel;
if (id !== channel_id) {
let channel = client.channels.get(id);
if (channel) {
if (channel.channel_type === 'TextChannel') {
history.push(`/server/${channel.server}/channel/${id}`);
} else {
history.push(`/channel/${id}`);
}
}
}
});
if (notif) {
notif.addEventListener("click", () => {
window.focus();
const id = msg.channel;
if (id !== channel_id) {
let channel = client.channels.get(id);
if (channel) {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server}/channel/${id}`,
);
} else {
history.push(`/channel/${id}`);
}
}
}
});
notifications[msg.channel] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel]
);
}
}
notifications[msg.channel] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel],
);
}
}
async function relationship(user: User, property: string) {
if (client.user?.status?.presence === Users.Presence.Busy) return;
if (property !== "relationship") return;
if (!showNotification) return;
async function relationship(user: User, property: string) {
if (client.user?.status?.presence === Users.Presence.Busy) return;
if (property !== "relationship") return;
if (!showNotification) return;
let event;
switch (user.relationship) {
case Users.Relationship.Incoming:
event = translate("notifications.sent_request", { person: user.username });
break;
case Users.Relationship.Friend:
event = translate("notifications.now_friends", { person: user.username });
break;
default:
return;
}
let event;
switch (user.relationship) {
case Users.Relationship.Incoming:
event = translate("notifications.sent_request", {
person: user.username,
});
break;
case Users.Relationship.Friend:
event = translate("notifications.now_friends", {
person: user.username,
});
break;
default:
return;
}
let notif = await createNotification(event, {
icon: client.users.getAvatarURL(user._id, { max_side: 256 }),
badge: '/assets/icons/android-chrome-512x512.png',
timestamp: +new Date()
});
let notif = await createNotification(event, {
icon: client.users.getAvatarURL(user._id, { max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(),
});
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
}
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
}
useEffect(() => {
client.addListener("message", message);
client.users.addListener("mutation", relationship);
useEffect(() => {
client.addListener("message", message);
client.users.addListener("mutation", relationship);
return () => {
client.removeListener("message", message);
client.users.removeListener("mutation", relationship);
};
}, [client, playSound, guild_id, channel_id, showNotification, notifs]);
return () => {
client.removeListener("message", message);
client.users.removeListener("mutation", relationship);
};
}, [client, playSound, guild_id, channel_id, showNotification, notifs]);
useEffect(() => {
function visChange() {
if (document.visibilityState === "visible") {
if (notifications[channel_id]) {
notifications[channel_id].close();
}
}
}
useEffect(() => {
function visChange() {
if (document.visibilityState === "visible") {
if (notifications[channel_id]) {
notifications[channel_id].close();
}
}
}
visChange();
visChange();
document.addEventListener("visibilitychange", visChange);
return () =>
document.removeEventListener("visibilitychange", visChange);
}, [guild_id, channel_id]);
document.addEventListener("visibilitychange", visChange);
return () =>
document.removeEventListener("visibilitychange", visChange);
}, [guild_id, channel_id]);
return null;
return null;
}
const NotifierComponent = connectState(
Notifier,
state => {
return {
options: state.settings.notification,
notifs: state.notifications
};
},
true
Notifier,
(state) => {
return {
options: state.settings.notification,
notifs: state.notifications,
};
},
true,
);
export default function Notifications() {
return (
<Switch>
<Route path="/server/:server/channel/:channel">
<NotifierComponent />
</Route>
<Route path="/channel/:channel">
<NotifierComponent />
</Route>
<Route path="/">
<NotifierComponent />
</Route>
</Switch>
);
export default function NotificationsComponent() {
return (
<Switch>
<Route path="/server/:server/channel/:channel">
<NotifierComponent />
</Route>
<Route path="/channel/:channel">
<NotifierComponent />
</Route>
<Route path="/">
<NotifierComponent />
</Route>
</Switch>
);
}

View File

@@ -1,44 +1,47 @@
import { Text } from "preact-i18n";
import styled from "styled-components";
import { useContext } from "preact/hooks";
import { Children } from "../../types/Preact";
import { WifiOff } from "@styled-icons/boxicons-regular";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useContext } from "preact/hooks";
import Preloader from "../../components/ui/Preloader";
import { Children } from "../../types/Preact";
import { ClientStatus, StatusContext } from "./RevoltClient";
interface Props {
children: Children;
children: Children;
}
const Base = styled.div`
gap: 16px;
padding: 1em;
display: flex;
user-select: none;
align-items: center;
flex-direction: row;
justify-content: center;
color: var(--tertiary-foreground);
background: var(--secondary-header);
gap: 16px;
padding: 1em;
display: flex;
user-select: none;
align-items: center;
flex-direction: row;
justify-content: center;
color: var(--tertiary-foreground);
background: var(--secondary-header);
> div {
font-size: 18px;
}
> div {
font-size: 18px;
}
`;
export default function RequiresOnline(props: Props) {
const status = useContext(StatusContext);
const status = useContext(StatusContext);
if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
return (
<Base>
<WifiOff size={16} />
<div>
<Text id="app.special.requires_online" />
</div>
</Base>
);
if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
return (
<Base>
<WifiOff size={16} />
<div>
<Text id="app.special.requires_online" />
</div>
</Base>
);
return <>{ props.children }</>;
return <>{props.children}</>;
}

View File

@@ -1,37 +1,42 @@
import { openDB } from 'idb';
import { openDB } from "idb";
import { useHistory } from "react-router-dom";
import { Client } from "revolt.js";
import { takeError } from "./util";
import { createContext } from "preact";
import { dispatch } from '../../redux';
import { Children } from "../../types/Preact";
import { useHistory } from 'react-router-dom';
import { Route } from "revolt.js/dist/api/routes";
import { connectState } from "../../redux/connector";
import Preloader from "../../components/ui/Preloader";
import { AuthState } from "../../redux/reducers/auth";
import { createContext } from "preact";
import { useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from '../intermediate/Intermediate';
import { SingletonMessageRenderer } from "../../lib/renderer/Singleton";
import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
import { AuthState } from "../../redux/reducers/auth";
import Preloader from "../../components/ui/Preloader";
import { Children } from "../../types/Preact";
import { useIntermediate } from "../intermediate/Intermediate";
import { registerEvents, setReconnectDisallowed } from "./events";
import { SingletonMessageRenderer } from '../../lib/renderer/Singleton';
import { takeError } from "./util";
export enum ClientStatus {
INIT,
LOADING,
READY,
OFFLINE,
DISCONNECTED,
CONNECTING,
RECONNECTING,
ONLINE,
INIT,
LOADING,
READY,
OFFLINE,
DISCONNECTED,
CONNECTING,
RECONNECTING,
ONLINE,
}
export interface ClientOperations {
login: (data: Route<"POST", "/auth/login">["data"]) => Promise<void>;
logout: (shouldRequest?: boolean) => Promise<void>;
loggedIn: () => boolean;
ready: () => boolean;
login: (data: Route<"POST", "/auth/login">["data"]) => Promise<void>;
logout: (shouldRequest?: boolean) => Promise<void>;
loggedIn: () => boolean;
ready: () => boolean;
openDM: (user_id: string) => Promise<string>;
openDM: (user_id: string) => Promise<string>;
}
// By the time they are used, they should all be initialized.
@@ -42,189 +47,195 @@ export const StatusContext = createContext<ClientStatus>(null!);
export const OperationsContext = createContext<ClientOperations>(null!);
type Props = {
auth: AuthState;
children: Children;
auth: AuthState;
children: Children;
};
function Context({ auth, children }: Props) {
const history = useHistory();
const { openScreen } = useIntermediate();
const [status, setStatus] = useState(ClientStatus.INIT);
const [client, setClient] = useState<Client>(undefined as unknown as Client);
const history = useHistory();
const { openScreen } = useIntermediate();
const [status, setStatus] = useState(ClientStatus.INIT);
const [client, setClient] = useState<Client>(
undefined as unknown as Client,
);
useEffect(() => {
(async () => {
let db;
try {
// Match sw.ts#L23
db = await openDB('state', 3, {
upgrade(db) {
for (let store of [ "channels", "servers", "users", "members" ]) {
db.createObjectStore(store, {
keyPath: '_id'
});
}
},
});
} catch (err) {
console.error('Failed to open IndexedDB store, continuing without.');
}
useEffect(() => {
(async () => {
let db;
try {
// Match sw.ts#L23
db = await openDB("state", 3, {
upgrade(db) {
for (let store of [
"channels",
"servers",
"users",
"members",
]) {
db.createObjectStore(store, {
keyPath: "_id",
});
}
},
});
} catch (err) {
console.error(
"Failed to open IndexedDB store, continuing without.",
);
}
const client = new Client({
autoReconnect: false,
apiURL: import.meta.env.VITE_API_URL,
debug: import.meta.env.DEV,
db
});
const client = new Client({
autoReconnect: false,
apiURL: import.meta.env.VITE_API_URL,
debug: import.meta.env.DEV,
db,
});
setClient(client);
SingletonMessageRenderer.subscribe(client);
setStatus(ClientStatus.LOADING);
})();
}, [ ]);
setClient(client);
SingletonMessageRenderer.subscribe(client);
setStatus(ClientStatus.LOADING);
})();
}, []);
if (status === ClientStatus.INIT) return null;
if (status === ClientStatus.INIT) return null;
const operations: ClientOperations = useMemo(() => {
return {
login: async data => {
setReconnectDisallowed(true);
const operations: ClientOperations = useMemo(() => {
return {
login: async (data) => {
setReconnectDisallowed(true);
try {
const onboarding = await client.login(data);
setReconnectDisallowed(false);
const login = () =>
dispatch({
type: "LOGIN",
session: client.session! // This [null assertion] is ok, we should have a session by now. - insert's words
});
try {
const onboarding = await client.login(data);
setReconnectDisallowed(false);
const login = () =>
dispatch({
type: "LOGIN",
session: client.session!, // This [null assertion] is ok, we should have a session by now. - insert's words
});
if (onboarding) {
openScreen({
id: "onboarding",
callback: (username: string) =>
onboarding(username, true).then(login)
});
} else {
login();
}
} catch (err) {
setReconnectDisallowed(false);
throw err;
}
},
logout: async shouldRequest => {
dispatch({ type: "LOGOUT" });
if (onboarding) {
openScreen({
id: "onboarding",
callback: (username: string) =>
onboarding(username, true).then(login),
});
} else {
login();
}
} catch (err) {
setReconnectDisallowed(false);
throw err;
}
},
logout: async (shouldRequest) => {
dispatch({ type: "LOGOUT" });
client.reset();
dispatch({ type: "RESET" });
client.reset();
dispatch({ type: "RESET" });
openScreen({ id: "none" });
setStatus(ClientStatus.READY);
openScreen({ id: "none" });
setStatus(ClientStatus.READY);
client.websocket.disconnect();
client.websocket.disconnect();
if (shouldRequest) {
try {
await client.logout();
} catch (err) {
console.error(err);
}
}
},
loggedIn: () => typeof auth.active !== "undefined",
ready: () => (
operations.loggedIn() &&
typeof client.user !== "undefined"
),
openDM: async (user_id: string) => {
let channel = await client.users.openDM(user_id);
history.push(`/channel/${channel!._id}`);
return channel!._id;
}
}
}, [ client, auth.active ]);
if (shouldRequest) {
try {
await client.logout();
} catch (err) {
console.error(err);
}
}
},
loggedIn: () => typeof auth.active !== "undefined",
ready: () =>
operations.loggedIn() && typeof client.user !== "undefined",
openDM: async (user_id: string) => {
let channel = await client.users.openDM(user_id);
history.push(`/channel/${channel!._id}`);
return channel!._id;
},
};
}, [client, auth.active]);
useEffect(() => registerEvents({ operations }, setStatus, client), [ client ]);
useEffect(
() => registerEvents({ operations }, setStatus, client),
[client],
);
useEffect(() => {
(async () => {
if (client.db) {
await client.restore();
}
useEffect(() => {
(async () => {
if (client.db) {
await client.restore();
}
if (auth.active) {
dispatch({ type: "QUEUE_FAIL_ALL" });
if (auth.active) {
dispatch({ type: "QUEUE_FAIL_ALL" });
const active = auth.accounts[auth.active];
client.user = client.users.get(active.session.user_id);
if (!navigator.onLine) {
return setStatus(ClientStatus.OFFLINE);
}
const active = auth.accounts[auth.active];
client.user = client.users.get(active.session.user_id);
if (!navigator.onLine) {
return setStatus(ClientStatus.OFFLINE);
}
if (operations.ready())
setStatus(ClientStatus.CONNECTING);
if (navigator.onLine) {
await client
.fetchConfiguration()
.catch(() =>
console.error("Failed to connect to API server.")
);
}
if (operations.ready()) setStatus(ClientStatus.CONNECTING);
try {
await client.fetchConfiguration();
const callback = await client.useExistingSession(
active.session
);
if (navigator.onLine) {
await client
.fetchConfiguration()
.catch(() =>
console.error("Failed to connect to API server."),
);
}
if (callback) {
openScreen({ id: "onboarding", callback });
}
} catch (err) {
setStatus(ClientStatus.DISCONNECTED);
const error = takeError(err);
if (error === "Forbidden" || error === "Unauthorized") {
operations.logout(true);
openScreen({ id: "signed_out" });
} else {
openScreen({ id: "error", error });
}
}
} else {
try {
await client.fetchConfiguration()
} catch (err) {
console.error("Failed to connect to API server.");
}
try {
await client.fetchConfiguration();
const callback = await client.useExistingSession(
active.session,
);
setStatus(ClientStatus.READY);
}
})();
}, []);
if (callback) {
openScreen({ id: "onboarding", callback });
}
} catch (err) {
setStatus(ClientStatus.DISCONNECTED);
const error = takeError(err);
if (error === "Forbidden" || error === "Unauthorized") {
operations.logout(true);
openScreen({ id: "signed_out" });
} else {
openScreen({ id: "error", error });
}
}
} else {
try {
await client.fetchConfiguration();
} catch (err) {
console.error("Failed to connect to API server.");
}
if (status === ClientStatus.LOADING) {
return <Preloader type="spinner" />;
}
setStatus(ClientStatus.READY);
}
})();
}, []);
return (
<AppContext.Provider value={client}>
<StatusContext.Provider value={status}>
<OperationsContext.Provider value={operations}>
{ children }
</OperationsContext.Provider>
</StatusContext.Provider>
</AppContext.Provider>
);
if (status === ClientStatus.LOADING) {
return <Preloader type="spinner" />;
}
return (
<AppContext.Provider value={client}>
<StatusContext.Provider value={status}>
<OperationsContext.Provider value={operations}>
{children}
</OperationsContext.Provider>
</StatusContext.Provider>
</AppContext.Provider>
);
}
export default connectState<{ children: Children }>(
Context,
state => {
return {
auth: state.auth,
sync: state.sync
};
}
);
export default connectState<{ children: Children }>(Context, (state) => {
return {
auth: state.auth,
sync: state.sync,
};
});

View File

@@ -1,77 +1,76 @@
/**
* This file monitors the message cache to delete any queued messages that have already sent.
*/
import { Message } from "revolt.js";
import { AppContext } from "./RevoltClient";
import { Typing } from "../../redux/reducers/typing";
import { useContext, useEffect } from "preact/hooks";
import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
import { QueuedMessage } from "../../redux/reducers/queue";
import { dispatch } from "../../redux";
import { Typing } from "../../redux/reducers/typing";
import { AppContext } from "./RevoltClient";
type Props = {
messages: QueuedMessage[];
typing: Typing
messages: QueuedMessage[];
typing: Typing;
};
function StateMonitor(props: Props) {
const client = useContext(AppContext);
const client = useContext(AppContext);
useEffect(() => {
dispatch({
type: 'QUEUE_DROP_ALL'
});
}, [ ]);
useEffect(() => {
dispatch({
type: "QUEUE_DROP_ALL",
});
}, []);
useEffect(() => {
function add(msg: Message) {
if (!msg.nonce) return;
if (!props.messages.find(x => x.id === msg.nonce)) return;
useEffect(() => {
function add(msg: Message) {
if (!msg.nonce) return;
if (!props.messages.find((x) => x.id === msg.nonce)) return;
dispatch({
type: 'QUEUE_REMOVE',
nonce: msg.nonce
});
}
dispatch({
type: "QUEUE_REMOVE",
nonce: msg.nonce,
});
}
client.addListener('message', add);
return () => client.removeListener('message', add);
}, [ props.messages ]);
client.addListener("message", add);
return () => client.removeListener("message", add);
}, [props.messages]);
useEffect(() => {
function removeOld() {
if (!props.typing) return;
for (let channel of Object.keys(props.typing)) {
let users = props.typing[channel];
useEffect(() => {
function removeOld() {
if (!props.typing) return;
for (let channel of Object.keys(props.typing)) {
let users = props.typing[channel];
for (let user of users) {
if (+ new Date() > user.started + 5000) {
dispatch({
type: 'TYPING_STOP',
channel,
user: user.id
});
}
}
}
}
for (let user of users) {
if (+new Date() > user.started + 5000) {
dispatch({
type: "TYPING_STOP",
channel,
user: user.id,
});
}
}
}
}
removeOld();
removeOld();
let interval = setInterval(removeOld, 1000);
return () => clearInterval(interval);
}, [ props.typing ]);
let interval = setInterval(removeOld, 1000);
return () => clearInterval(interval);
}, [props.typing]);
return null;
return null;
}
export default connectState(
StateMonitor,
state => {
return {
messages: [...state.queue],
typing: state.typing
};
}
);
export default connectState(StateMonitor, (state) => {
return {
messages: [...state.queue],
typing: state.typing,
};
});

View File

@@ -1,126 +1,146 @@
/**
* This file monitors changes to settings and syncs them to the server.
*/
import isEqual from "lodash.isequal";
import { Language } from "../Locale";
import { Sync } from "revolt.js/dist/api/objects";
import { useContext, useEffect } from "preact/hooks";
import { connectState } from "../../redux/connector";
import { Settings } from "../../redux/reducers/settings";
import { Notifications } from "../../redux/reducers/notifications";
import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { DEFAULT_ENABLED_SYNC, SyncData, SyncKeys, SyncOptions } from "../../redux/reducers/sync";
import { useContext, useEffect } from "preact/hooks";
import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
import { Notifications } from "../../redux/reducers/notifications";
import { Settings } from "../../redux/reducers/settings";
import {
DEFAULT_ENABLED_SYNC,
SyncData,
SyncKeys,
SyncOptions,
} from "../../redux/reducers/sync";
import { Language } from "../Locale";
import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
type Props = {
settings: Settings,
locale: Language,
sync: SyncOptions,
notifications: Notifications
settings: Settings;
locale: Language;
sync: SyncOptions;
notifications: Notifications;
};
var lastValues: { [key in SyncKeys]?: any } = { };
var lastValues: { [key in SyncKeys]?: any } = {};
export function mapSync(packet: Sync.UserSettings, revision?: Record<string, number>) {
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
for (let key of Object.keys(packet)) {
let [ timestamp, obj ] = packet[key];
if (timestamp < (revision ?? {})[key] ?? 0) {
continue;
}
export function mapSync(
packet: Sync.UserSettings,
revision?: Record<string, number>,
) {
let update: { [key in SyncKeys]?: [number, SyncData[key]] } = {};
for (let key of Object.keys(packet)) {
let [timestamp, obj] = packet[key];
if (timestamp < (revision ?? {})[key] ?? 0) {
continue;
}
let object;
if (obj[0] === '{') {
object = JSON.parse(obj)
} else {
object = obj;
}
let object;
if (obj[0] === "{") {
object = JSON.parse(obj);
} else {
object = obj;
}
lastValues[key as SyncKeys] = object;
update[key as SyncKeys] = [ timestamp, object ];
}
lastValues[key as SyncKeys] = object;
update[key as SyncKeys] = [timestamp, object];
}
return update;
return update;
}
function SyncManager(props: Props) {
const client = useContext(AppContext);
const status = useContext(StatusContext);
const client = useContext(AppContext);
const status = useContext(StatusContext);
useEffect(() => {
if (status === ClientStatus.ONLINE) {
client
.syncFetchSettings(DEFAULT_ENABLED_SYNC.filter(x => !props.sync?.disabled?.includes(x)))
.then(data => {
dispatch({
type: 'SYNC_UPDATE',
update: mapSync(data)
});
});
useEffect(() => {
if (status === ClientStatus.ONLINE) {
client
.syncFetchSettings(
DEFAULT_ENABLED_SYNC.filter(
(x) => !props.sync?.disabled?.includes(x),
),
)
.then((data) => {
dispatch({
type: "SYNC_UPDATE",
update: mapSync(data),
});
});
client
.syncFetchUnreads()
.then(unreads => dispatch({ type: 'UNREADS_SET', unreads }));
}
}, [ status ]);
client
.syncFetchUnreads()
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
}
}, [status]);
function syncChange(key: SyncKeys, data: any) {
let timestamp = + new Date();
dispatch({
type: 'SYNC_SET_REVISION',
key,
timestamp
});
function syncChange(key: SyncKeys, data: any) {
let timestamp = +new Date();
dispatch({
type: "SYNC_SET_REVISION",
key,
timestamp,
});
client.syncSetSettings({
[key]: data
}, timestamp);
}
client.syncSetSettings(
{
[key]: data,
},
timestamp,
);
}
let disabled = props.sync.disabled ?? [];
for (let [key, object] of [ ['appearance', props.settings.appearance], ['theme', props.settings.theme], ['locale', props.locale], ['notifications', props.notifications] ] as [SyncKeys, any][]) {
useEffect(() => {
if (disabled.indexOf(key) === -1) {
if (typeof lastValues[key] !== 'undefined') {
if (!isEqual(lastValues[key], object)) {
syncChange(key, object);
}
}
}
let disabled = props.sync.disabled ?? [];
for (let [key, object] of [
["appearance", props.settings.appearance],
["theme", props.settings.theme],
["locale", props.locale],
["notifications", props.notifications],
] as [SyncKeys, any][]) {
useEffect(() => {
if (disabled.indexOf(key) === -1) {
if (typeof lastValues[key] !== "undefined") {
if (!isEqual(lastValues[key], object)) {
syncChange(key, object);
}
}
}
lastValues[key] = object;
}, [ disabled, object ]);
}
lastValues[key] = object;
}, [disabled, object]);
}
useEffect(() => {
function onPacket(packet: ClientboundNotification) {
if (packet.type === 'UserSettingsUpdate') {
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = mapSync(packet.update, props.sync.revision);
useEffect(() => {
function onPacket(packet: ClientboundNotification) {
if (packet.type === "UserSettingsUpdate") {
let update: { [key in SyncKeys]?: [number, SyncData[key]] } =
mapSync(packet.update, props.sync.revision);
dispatch({
type: 'SYNC_UPDATE',
update
});
}
}
dispatch({
type: "SYNC_UPDATE",
update,
});
}
}
client.addListener('packet', onPacket);
return () => client.removeListener('packet', onPacket);
}, [ disabled, props.sync ]);
client.addListener("packet", onPacket);
return () => client.removeListener("packet", onPacket);
}, [disabled, props.sync]);
return null;
return null;
}
export default connectState(
SyncManager,
state => {
return {
settings: state.settings,
locale: state.locale,
sync: state.sync,
notifications: state.notifications
};
}
);
export default connectState(SyncManager, (state) => {
return {
settings: state.settings,
locale: state.locale,
sync: state.sync,
notifications: state.notifications,
};
});

View File

@@ -1,148 +1,155 @@
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { Client, Message } from "revolt.js/dist";
import {
ClientOperations,
ClientStatus
} from "./RevoltClient";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { StateUpdater } from "preact/hooks";
import { dispatch } from "../../redux";
import { ClientOperations, ClientStatus } from "./RevoltClient";
export var preventReconnect = false;
let preventUntil = 0;
export function setReconnectDisallowed(allowed: boolean) {
preventReconnect = allowed;
preventReconnect = allowed;
}
export function registerEvents({
operations
}: { operations: ClientOperations }, setStatus: StateUpdater<ClientStatus>, client: Client) {
function attemptReconnect() {
if (preventReconnect) return;
function reconnect() {
preventUntil = +new Date() + 2000;
client.websocket.connect().catch(err => console.error(err));
}
export function registerEvents(
{ operations }: { operations: ClientOperations },
setStatus: StateUpdater<ClientStatus>,
client: Client,
) {
function attemptReconnect() {
if (preventReconnect) return;
function reconnect() {
preventUntil = +new Date() + 2000;
client.websocket.connect().catch((err) => console.error(err));
}
if (+new Date() > preventUntil) {
setTimeout(reconnect, 2000);
} else {
reconnect();
}
}
if (+new Date() > preventUntil) {
setTimeout(reconnect, 2000);
} else {
reconnect();
}
}
let listeners: Record<string, (...args: any[]) => void> = {
connecting: () =>
operations.ready() && setStatus(ClientStatus.CONNECTING),
let listeners: Record<string, (...args: any[]) => void> = {
connecting: () =>
operations.ready() && setStatus(ClientStatus.CONNECTING),
dropped: () => {
if (operations.ready()) {
setStatus(ClientStatus.DISCONNECTED);
attemptReconnect();
}
},
dropped: () => {
if (operations.ready()) {
setStatus(ClientStatus.DISCONNECTED);
attemptReconnect();
}
},
packet: (packet: ClientboundNotification) => {
switch (packet.type) {
case "ChannelStartTyping": {
if (packet.user === client.user?._id) return;
dispatch({
type: "TYPING_START",
channel: packet.id,
user: packet.user
});
break;
}
case "ChannelStopTyping": {
if (packet.user === client.user?._id) return;
dispatch({
type: "TYPING_STOP",
channel: packet.id,
user: packet.user
});
break;
}
case "ChannelAck": {
dispatch({
type: "UNREADS_MARK_READ",
channel: packet.id,
message: packet.message_id
});
break;
}
}
},
packet: (packet: ClientboundNotification) => {
switch (packet.type) {
case "ChannelStartTyping": {
if (packet.user === client.user?._id) return;
dispatch({
type: "TYPING_START",
channel: packet.id,
user: packet.user,
});
break;
}
case "ChannelStopTyping": {
if (packet.user === client.user?._id) return;
dispatch({
type: "TYPING_STOP",
channel: packet.id,
user: packet.user,
});
break;
}
case "ChannelAck": {
dispatch({
type: "UNREADS_MARK_READ",
channel: packet.id,
message: packet.message_id,
});
break;
}
}
},
message: (message: Message) => {
if (message.mentions?.includes(client.user!._id)) {
dispatch({
type: "UNREADS_MENTION",
channel: message.channel,
message: message._id
});
}
},
message: (message: Message) => {
if (message.mentions?.includes(client.user!._id)) {
dispatch({
type: "UNREADS_MENTION",
channel: message.channel,
message: message._id,
});
}
},
ready: () => setStatus(ClientStatus.ONLINE)
};
ready: () => setStatus(ClientStatus.ONLINE),
};
if (import.meta.env.DEV) {
listeners = new Proxy(listeners, {
get: (target, listener, receiver) => (...args: unknown[]) => {
console.debug(`Calling ${listener.toString()} with`, args);
Reflect.get(target, listener)(...args)
}
})
}
if (import.meta.env.DEV) {
listeners = new Proxy(listeners, {
get:
(target, listener, receiver) =>
(...args: unknown[]) => {
console.debug(`Calling ${listener.toString()} with`, args);
Reflect.get(target, listener)(...args);
},
});
}
// TODO: clean this a bit and properly handle types
for (const listener in listeners) {
client.addListener(listener, listeners[listener]);
}
// TODO: clean this a bit and properly handle types
for (const listener in listeners) {
client.addListener(listener, listeners[listener]);
}
function logMutation(target: string, key: string) {
console.log('(o) Object mutated', target, '\nChanged:', key);
}
function logMutation(target: string, key: string) {
console.log("(o) Object mutated", target, "\nChanged:", key);
}
if (import.meta.env.DEV) {
client.users.addListener('mutation', logMutation);
client.servers.addListener('mutation', logMutation);
client.channels.addListener('mutation', logMutation);
client.servers.members.addListener('mutation', logMutation);
}
if (import.meta.env.DEV) {
client.users.addListener("mutation", logMutation);
client.servers.addListener("mutation", logMutation);
client.channels.addListener("mutation", logMutation);
client.servers.members.addListener("mutation", logMutation);
}
const online = () => {
if (operations.ready()) {
setStatus(ClientStatus.RECONNECTING);
setReconnectDisallowed(false);
attemptReconnect();
}
};
const offline = () => {
if (operations.ready()) {
setReconnectDisallowed(true);
client.websocket.disconnect();
setStatus(ClientStatus.OFFLINE);
}
};
const online = () => {
if (operations.ready()) {
setStatus(ClientStatus.RECONNECTING);
setReconnectDisallowed(false);
attemptReconnect();
}
};
window.addEventListener("online", online);
window.addEventListener("offline", offline);
const offline = () => {
if (operations.ready()) {
setReconnectDisallowed(true);
client.websocket.disconnect();
setStatus(ClientStatus.OFFLINE);
}
};
return () => {
for (const listener in listeners) {
client.removeListener(listener, listeners[listener as keyof typeof listeners]);
}
window.addEventListener("online", online);
window.addEventListener("offline", offline);
if (import.meta.env.DEV) {
client.users.removeListener('mutation', logMutation);
client.servers.removeListener('mutation', logMutation);
client.channels.removeListener('mutation', logMutation);
client.servers.members.removeListener('mutation', logMutation);
}
return () => {
for (const listener in listeners) {
client.removeListener(
listener,
listeners[listener as keyof typeof listeners],
);
}
window.removeEventListener("online", online);
window.removeEventListener("offline", offline);
};
if (import.meta.env.DEV) {
client.users.removeListener("mutation", logMutation);
client.servers.removeListener("mutation", logMutation);
client.channels.removeListener("mutation", logMutation);
client.servers.members.removeListener("mutation", logMutation);
}
window.removeEventListener("online", online);
window.removeEventListener("offline", offline);
};
}

View File

@@ -1,177 +1,232 @@
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { Client, PermissionCalculator } from "revolt.js";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
import { Client, PermissionCalculator } from 'revolt.js';
import { AppContext } from "./RevoltClient";
import Collection from "revolt.js/dist/maps/Collection";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "./RevoltClient";
export interface HookContext {
client: Client,
forceUpdate: () => void
client: Client;
forceUpdate: () => void;
}
export function useForceUpdate(context?: HookContext): HookContext {
const client = useContext(AppContext);
if (context) return context;
const client = useContext(AppContext);
if (context) return context;
const H = useState(0);
var updateState: (_: number) => void;
if (Array.isArray(H)) {
let [, u] = H;
updateState = u;
} else {
console.warn('Failed to construct using useState.');
updateState = ()=>{};
}
const H = useState(0);
var updateState: (_: number) => void;
if (Array.isArray(H)) {
let [, u] = H;
updateState = u;
} else {
console.warn("Failed to construct using useState.");
updateState = () => {};
}
return { client, forceUpdate: () => updateState(Math.random()) };
return { client, forceUpdate: () => updateState(Math.random()) };
}
// TODO: utils.d.ts maybe?
type PickProperties<T, U> = Pick<T, {
[K in keyof T]: T[K] extends U ? K : never
}[keyof T]>
type PickProperties<T, U> = Pick<
T,
{
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T]
>;
// The keys in Client that are an object
// for some reason undefined keeps appearing despite there being no reason to so it's filtered out
type ClientCollectionKey = Exclude<keyof PickProperties<Client, Collection<any>>, undefined>;
type ClientCollectionKey = Exclude<
keyof PickProperties<Client, Collection<any>>,
undefined
>;
function useObject(type: ClientCollectionKey, id?: string | string[], context?: HookContext) {
const ctx = useForceUpdate(context);
function useObject(
type: ClientCollectionKey,
id?: string | string[],
context?: HookContext,
) {
const ctx = useForceUpdate(context);
function update(target: any) {
if (typeof id === 'string' ? target === id :
Array.isArray(id) ? id.includes(target) : true) {
ctx.forceUpdate();
}
}
function update(target: any) {
if (
typeof id === "string"
? target === id
: Array.isArray(id)
? id.includes(target)
: true
) {
ctx.forceUpdate();
}
}
const map = ctx.client[type];
useEffect(() => {
map.addListener("update", update);
return () => map.removeListener("update", update);
}, [id]);
const map = ctx.client[type];
useEffect(() => {
map.addListener("update", update);
return () => map.removeListener("update", update);
}, [id]);
return typeof id === 'string' ? map.get(id)
: Array.isArray(id) ? id.map(x => map.get(x))
: map.toArray();
return typeof id === "string"
? map.get(id)
: Array.isArray(id)
? id.map((x) => map.get(x))
: map.toArray();
}
export function useUser(id?: string, context?: HookContext) {
if (typeof id === "undefined") return;
return useObject('users', id, context) as Readonly<Users.User> | undefined;
if (typeof id === "undefined") return;
return useObject("users", id, context) as Readonly<Users.User> | undefined;
}
export function useSelf(context?: HookContext) {
const ctx = useForceUpdate(context);
return useUser(ctx.client.user!._id, ctx);
const ctx = useForceUpdate(context);
return useUser(ctx.client.user!._id, ctx);
}
export function useUsers(ids?: string[], context?: HookContext) {
return useObject('users', ids, context) as (Readonly<Users.User> | undefined)[];
return useObject("users", ids, context) as (
| Readonly<Users.User>
| undefined
)[];
}
export function useChannel(id?: string, context?: HookContext) {
if (typeof id === "undefined") return;
return useObject('channels', id, context) as Readonly<Channels.Channel> | undefined;
if (typeof id === "undefined") return;
return useObject("channels", id, context) as
| Readonly<Channels.Channel>
| undefined;
}
export function useChannels(ids?: string[], context?: HookContext) {
return useObject('channels', ids, context) as (Readonly<Channels.Channel> | undefined)[];
return useObject("channels", ids, context) as (
| Readonly<Channels.Channel>
| undefined
)[];
}
export function useServer(id?: string, context?: HookContext) {
if (typeof id === "undefined") return;
return useObject('servers', id, context) as Readonly<Servers.Server> | undefined;
if (typeof id === "undefined") return;
return useObject("servers", id, context) as
| Readonly<Servers.Server>
| undefined;
}
export function useServers(ids?: string[], context?: HookContext) {
return useObject('servers', ids, context) as (Readonly<Servers.Server> | undefined)[];
return useObject("servers", ids, context) as (
| Readonly<Servers.Server>
| undefined
)[];
}
export function useDMs(context?: HookContext) {
const ctx = useForceUpdate(context);
const ctx = useForceUpdate(context);
function mutation(target: string) {
let channel = ctx.client.channels.get(target);
if (channel) {
if (channel.channel_type === 'DirectMessage' || channel.channel_type === 'Group') {
ctx.forceUpdate();
}
}
}
function mutation(target: string) {
let channel = ctx.client.channels.get(target);
if (channel) {
if (
channel.channel_type === "DirectMessage" ||
channel.channel_type === "Group"
) {
ctx.forceUpdate();
}
}
}
const map = ctx.client.channels;
useEffect(() => {
map.addListener("update", mutation);
return () => map.removeListener("update", mutation);
}, []);
const map = ctx.client.channels;
useEffect(() => {
map.addListener("update", mutation);
return () => map.removeListener("update", mutation);
}, []);
return map
.toArray()
.filter(x => x.channel_type === 'DirectMessage' || x.channel_type === 'Group' || x.channel_type === 'SavedMessages') as (Channels.GroupChannel | Channels.DirectMessageChannel | Channels.SavedMessagesChannel)[];
return map
.toArray()
.filter(
(x) =>
x.channel_type === "DirectMessage" ||
x.channel_type === "Group" ||
x.channel_type === "SavedMessages",
) as (
| Channels.GroupChannel
| Channels.DirectMessageChannel
| Channels.SavedMessagesChannel
)[];
}
export function useUserPermission(id: string, context?: HookContext) {
const ctx = useForceUpdate(context);
const ctx = useForceUpdate(context);
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
useEffect(() => {
ctx.client.users.addListener("update", mutation);
return () => ctx.client.users.removeListener("update", mutation);
}, [id]);
let calculator = new PermissionCalculator(ctx.client);
return calculator.forUser(id);
const mutation = (target: string) => target === id && ctx.forceUpdate();
useEffect(() => {
ctx.client.users.addListener("update", mutation);
return () => ctx.client.users.removeListener("update", mutation);
}, [id]);
let calculator = new PermissionCalculator(ctx.client);
return calculator.forUser(id);
}
export function useChannelPermission(id: string, context?: HookContext) {
const ctx = useForceUpdate(context);
const ctx = useForceUpdate(context);
const channel = ctx.client.channels.get(id);
const server = (channel && (channel.channel_type === 'TextChannel' || channel.channel_type === 'VoiceChannel')) ? channel.server : undefined;
const channel = ctx.client.channels.get(id);
const server =
channel &&
(channel.channel_type === "TextChannel" ||
channel.channel_type === "VoiceChannel")
? channel.server
: undefined;
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
const mutationServer = (target: string) => (target === server) && ctx.forceUpdate();
const mutationMember = (target: string) => (target.substr(26) === ctx.client.user!._id) && ctx.forceUpdate();
const mutation = (target: string) => target === id && ctx.forceUpdate();
const mutationServer = (target: string) =>
target === server && ctx.forceUpdate();
const mutationMember = (target: string) =>
target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
useEffect(() => {
ctx.client.channels.addListener("update", mutation);
useEffect(() => {
ctx.client.channels.addListener("update", mutation);
if (server) {
ctx.client.servers.addListener("update", mutationServer);
ctx.client.servers.members.addListener("update", mutationMember);
}
if (server) {
ctx.client.servers.addListener("update", mutationServer);
ctx.client.servers.members.addListener("update", mutationMember);
}
return () => {
ctx.client.channels.removeListener("update", mutation);
return () => {
ctx.client.channels.removeListener("update", mutation);
if (server) {
ctx.client.servers.removeListener("update", mutationServer);
ctx.client.servers.members.removeListener("update", mutationMember);
}
}
}, [id]);
let calculator = new PermissionCalculator(ctx.client);
return calculator.forChannel(id);
if (server) {
ctx.client.servers.removeListener("update", mutationServer);
ctx.client.servers.members.removeListener(
"update",
mutationMember,
);
}
};
}, [id]);
let calculator = new PermissionCalculator(ctx.client);
return calculator.forChannel(id);
}
export function useServerPermission(id: string, context?: HookContext) {
const ctx = useForceUpdate(context);
const ctx = useForceUpdate(context);
const mutation = (target: string) => (target === id) && ctx.forceUpdate();
const mutationMember = (target: string) => (target.substr(26) === ctx.client.user!._id) && ctx.forceUpdate();
const mutation = (target: string) => target === id && ctx.forceUpdate();
const mutationMember = (target: string) =>
target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();
useEffect(() => {
ctx.client.servers.addListener("update", mutation);
ctx.client.servers.members.addListener("update", mutationMember);
useEffect(() => {
ctx.client.servers.addListener("update", mutation);
ctx.client.servers.members.addListener("update", mutationMember);
return () => {
ctx.client.servers.removeListener("update", mutation);
ctx.client.servers.members.removeListener("update", mutationMember);
}
}, [id]);
let calculator = new PermissionCalculator(ctx.client);
return calculator.forServer(id);
return () => {
ctx.client.servers.removeListener("update", mutation);
ctx.client.servers.members.removeListener("update", mutationMember);
};
}, [id]);
let calculator = new PermissionCalculator(ctx.client);
return calculator.forServer(id);
}

View File

@@ -1,48 +1,57 @@
import { Channel, Message, User } from "revolt.js/dist/api/objects";
import { Children } from "../../types/Preact";
import { Text } from "preact-i18n";
import { Client } from "revolt.js";
import { Channel, Message, User } from "revolt.js/dist/api/objects";
export function takeError(
error: any
): string {
const type = error?.response?.data?.type;
let id = type;
if (!type) {
if (error?.response?.status === 403) {
return "Unauthorized";
} else if (error && (!!error.isAxiosError && !error.response)) {
return "NetworkError";
}
import { Text } from "preact-i18n";
console.error(error);
return "UnknownError";
}
import { Children } from "../../types/Preact";
return id;
export function takeError(error: any): string {
const type = error?.response?.data?.type;
let id = type;
if (!type) {
if (error?.response?.status === 403) {
return "Unauthorized";
} else if (error && !!error.isAxiosError && !error.response) {
return "NetworkError";
}
console.error(error);
return "UnknownError";
}
return id;
}
export function getChannelName(client: Client, channel: Channel, prefixType?: boolean): Children {
if (channel.channel_type === "SavedMessages")
return <Text id="app.navigation.tabs.saved" />;
export function getChannelName(
client: Client,
channel: Channel,
prefixType?: boolean,
): Children {
if (channel.channel_type === "SavedMessages")
return <Text id="app.navigation.tabs.saved" />;
if (channel.channel_type === "DirectMessage") {
let uid = client.channels.getRecipient(channel._id);
return <>{prefixType && "@"}{client.users.get(uid)?.username}</>;
}
if (channel.channel_type === "DirectMessage") {
let uid = client.channels.getRecipient(channel._id);
return (
<>
{prefixType && "@"}
{client.users.get(uid)?.username}
</>
);
}
if (channel.channel_type === "TextChannel" && prefixType) {
return <>#{channel.name}</>;
}
if (channel.channel_type === "TextChannel" && prefixType) {
return <>#{channel.name}</>;
}
return <>{channel.name}</>;
return <>{channel.name}</>;
}
export type MessageObject = Omit<Message, "edited"> & { edited?: string };
export function mapMessage(message: Partial<Message>) {
const { edited, ...msg } = message;
return {
...msg,
edited: edited?.$date,
} as MessageObject;
const { edited, ...msg } = message;
return {
...msg,
edited: edited?.$date,
} as MessageObject;
}