Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 597 additions and 816 deletions
import { Plus, X, XCircle } from "@styled-icons/boxicons-regular"; import { Plus } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid"; import { Pencil } from "@styled-icons/boxicons-solid";
import Axios, { AxiosRequestConfig } from "axios"; import Axios, { AxiosRequestConfig } from "axios";
...@@ -55,7 +55,7 @@ export async function uploadFile( ...@@ -55,7 +55,7 @@ export async function uploadFile(
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
const res = await Axios.post(autumnURL + "/" + tag, formData, { const res = await Axios.post(`${autumnURL}/${tag}`, formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
...@@ -78,7 +78,7 @@ export function grabFiles( ...@@ -78,7 +78,7 @@ export function grabFiles(
input.onchange = async (e) => { input.onchange = async (e) => {
const files = (e.currentTarget as HTMLInputElement)?.files; const files = (e.currentTarget as HTMLInputElement)?.files;
if (!files) return; if (!files) return;
for (let file of files) { for (const file of files) {
if (file.size > maxFileSize) { if (file.size > maxFileSize) {
return tooLarge(); return tooLarge();
} }
...@@ -139,16 +139,15 @@ export function FileUploader(props: Props) { ...@@ -139,16 +139,15 @@ export function FileUploader(props: Props) {
} else { } else {
onClick(); onClick();
} }
} else if (props.previewURL) {
props.remove();
} else { } else {
if (props.previewURL) { onClick();
props.remove();
} else {
onClick();
}
} }
} }
if (props.behaviour === "multi" && props.append) { if (props.behaviour === "multi" && props.append) {
// eslint-disable-next-line
useEffect(() => { useEffect(() => {
// File pasting. // File pasting.
function paste(e: ClipboardEvent) { function paste(e: ClipboardEvent) {
...@@ -156,7 +155,7 @@ export function FileUploader(props: Props) { ...@@ -156,7 +155,7 @@ export function FileUploader(props: Props) {
if (typeof items === "undefined") return; if (typeof items === "undefined") return;
if (props.behaviour !== "multi" || !props.append) return; if (props.behaviour !== "multi" || !props.append) return;
let files = []; const files = [];
for (const item of items) { for (const item of items) {
if (!item.type.startsWith("text/")) { if (!item.type.startsWith("text/")) {
const blob = item.getAsFile(); const blob = item.getAsFile();
...@@ -190,7 +189,7 @@ export function FileUploader(props: Props) { ...@@ -190,7 +189,7 @@ export function FileUploader(props: Props) {
const dropped = e.dataTransfer?.files; const dropped = e.dataTransfer?.files;
if (dropped) { if (dropped) {
let files = []; const files = [];
for (const item of dropped) { for (const item of dropped) {
if (item.size > props.maxFileSize) { if (item.size > props.maxFileSize) {
openScreen({ id: "error", error: "FileTooLarge" }); openScreen({ id: "error", error: "FileTooLarge" });
...@@ -212,7 +211,7 @@ export function FileUploader(props: Props) { ...@@ -212,7 +211,7 @@ export function FileUploader(props: Props) {
document.removeEventListener("dragover", dragover); document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop); document.removeEventListener("drop", drop);
}; };
}, [props.append]); }, [openScreen, props, props.append]);
} }
if (props.style === "icon" || props.style === "banner") { if (props.style === "icon" || props.style === "banner") {
...@@ -277,7 +276,7 @@ export function FileUploader(props: Props) { ...@@ -277,7 +276,7 @@ export function FileUploader(props: Props) {
if (attached) return remove(); if (attached) return remove();
onClick(); onClick();
}} }}
rotate={uploading || attached ? '45deg' : undefined}> rotate={uploading || attached ? "45deg" : undefined}>
<Plus size={size} /> <Plus size={size} />
</IconButton> </IconButton>
); );
......
import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Message, SYSTEM_USER_ID, User } from "revolt.js"; import { Presence, RelationshipStatus } from "revolt-api/types/Users";
import { Users } from "revolt.js/dist/api/objects"; import { SYSTEM_USER_ID } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js/dist/maps/Users";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { useContext, useEffect } from "preact/hooks"; import { useCallback, useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n"; import { useTranslation } from "../../lib/i18n";
...@@ -32,7 +34,7 @@ async function createNotification( ...@@ -32,7 +34,7 @@ async function createNotification(
try { try {
return new Notification(title, options); return new Notification(title, options);
} catch (err) { } catch (err) {
let sw = await navigator.serviceWorker.getRegistration(); const sw = await navigator.serviceWorker.getRegistration();
sw?.showNotification(title, options); sw?.showNotification(title, options);
} }
} }
...@@ -49,195 +51,225 @@ function Notifier({ options, notifs }: Props) { ...@@ -49,195 +51,225 @@ function Notifier({ options, notifs }: Props) {
const history = useHistory(); const history = useHistory();
const playSound = useContext(SoundContext); const playSound = useContext(SoundContext);
async function message(msg: Message) { const message = useCallback(
if (msg.author === client.user!._id) return; async (msg: Message) => {
if (msg.channel === channel_id && document.hasFocus()) return; if (msg.author_id === client.user!._id) return;
if (client.user!.status?.presence === Users.Presence.Busy) return; if (msg.channel_id === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Presence.Busy) return;
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
const channel = client.channels.get(msg.channel); const notifState = getNotificationState(notifs, msg.channel!);
const author = client.users.get(msg.author); if (!shouldNotify(notifState, msg, client.user!._id)) return;
if (!channel) return;
if (author?.relationship === Users.Relationship.Blocked) return;
const notifState = getNotificationState(notifs, channel); playSound("message");
if (!shouldNotify(notifState, msg, client.user!._id)) return; if (!showNotification) return;
playSound("message"); let title;
if (!showNotification) return; switch (msg.channel?.channel_type) {
case "SavedMessages":
let title; return;
switch (channel.channel_type) { case "DirectMessage":
case "SavedMessages": title = `@${msg.author?.username}`;
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 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; break;
case "user_joined": case "Group":
case "user_left": if (msg.author?._id === SYSTEM_USER_ID) {
case "user_kicked": title = msg.channel.name;
case "user_banned": } else {
body = translate( title = `@${msg.author?.username} - ${msg.channel.name}`;
`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; break;
case "channel_renamed": case "TextChannel":
body = translate( title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
`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; break;
case "channel_description_changed": default:
case "channel_icon_changed": title = msg.channel?._id;
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; break;
} }
}
let notif = await createNotification(title, { let image;
icon, if (msg.attachments) {
image, const imageAttachment = msg.attachments.find(
body, (x) => x.metadata.type === "Image",
timestamp: decodeTime(msg._id), );
tag: msg.channel, if (imageAttachment) {
badge: "/assets/icons/android-chrome-512x512.png", image = client.generateFileURL(imageAttachment, {
silent: true, max_side: 720,
}); });
}
}
if (notif) { let body, icon;
notif.addEventListener("click", () => { if (typeof msg.content === "string") {
window.focus(); body = client.markdownToText(msg.content);
const id = msg.channel; icon = msg.author?.generateAvatarURL({ max_side: 256 });
if (id !== channel_id) { } else {
let channel = client.channels.get(id); const users = client.users;
if (channel) { switch (msg.content.type) {
if (channel.channel_type === "TextChannel") { case "user_added":
history.push( case "user_remove":
`/server/${channel.server}/channel/${id}`, {
const user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: user?.username,
other_user: users.get(msg.content.by)
?.username,
},
); );
} else { icon = user?.generateAvatarURL({
history.push(`/channel/${id}`); max_side: 256,
});
} }
} break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
{
const user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: user?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_renamed":
{
const user = users.get(msg.content.by);
body = translate(
`app.main.channel.system.channel_renamed`,
{
user: users.get(msg.content.by)?.username,
name: msg.content.name,
},
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_description_changed":
case "channel_icon_changed":
{
const user = users.get(msg.content.by);
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.by)?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
} }
}
const notif = await createNotification(title!, {
icon,
image,
body,
timestamp: decodeTime(msg._id),
tag: msg.channel?._id,
badge: "/assets/icons/android-chrome-512x512.png",
silent: true,
}); });
notifications[msg.channel] = notif; if (notif) {
notif.addEventListener( notif.addEventListener("click", () => {
"close", window.focus();
() => delete notifications[msg.channel], const id = msg.channel_id;
); if (id !== channel_id) {
} const channel = client.channels.get(id);
} if (channel) {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${id}`,
);
} else {
history.push(`/channel/${id}`);
}
}
}
});
notifications[msg.channel_id] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel_id],
);
}
},
[
history,
showNotification,
translate,
channel_id,
client,
notifs,
playSound,
],
);
async function relationship(user: User, property: string) { const relationship = useCallback(
if (client.user?.status?.presence === Users.Presence.Busy) return; async (user: User) => {
if (property !== "relationship") return; if (client.user?.status?.presence === Presence.Busy) return;
if (!showNotification) return; if (!showNotification) return;
let event; let event;
switch (user.relationship) { switch (user.relationship) {
case Users.Relationship.Incoming: case RelationshipStatus.Incoming:
event = translate("notifications.sent_request", { event = translate("notifications.sent_request", {
person: user.username, person: user.username,
}); });
break; break;
case Users.Relationship.Friend: case RelationshipStatus.Friend:
event = translate("notifications.now_friends", { event = translate("notifications.now_friends", {
person: user.username, person: user.username,
}); });
break; break;
default: default:
return; return;
} }
let notif = await createNotification(event, { const notif = await createNotification(event, {
icon: client.users.getAvatarURL(user._id, { max_side: 256 }), icon: user.generateAvatarURL({ max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png", badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(), timestamp: +new Date(),
}); });
notif?.addEventListener("click", () => { notif?.addEventListener("click", () => {
history.push(`/friends`); history.push(`/friends`);
}); });
} },
[client.user?.status?.presence, history, showNotification, translate],
);
useEffect(() => { useEffect(() => {
client.addListener("message", message); client.addListener("message", message);
client.users.addListener("mutation", relationship); client.addListener("user/relationship", relationship);
return () => { return () => {
client.removeListener("message", message); client.removeListener("message", message);
client.users.removeListener("mutation", relationship); client.removeListener("user/relationship", relationship);
}; };
}, [client, playSound, guild_id, channel_id, showNotification, notifs]); }, [
client,
playSound,
guild_id,
channel_id,
showNotification,
notifs,
message,
relationship,
]);
useEffect(() => { useEffect(() => {
function visChange() { function visChange() {
......
import { openDB } from "idb"; /* eslint-disable react-hooks/rules-of-hooks */
import { useHistory } from "react-router-dom";
import { Client } from "revolt.js"; import { Client } from "revolt.js";
import { Route } from "revolt.js/dist/api/routes"; import { Route } from "revolt.js/dist/api/routes";
import { createContext } from "preact"; import { createContext } from "preact";
import { useEffect, useMemo, useState } from "preact/hooks"; import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { SingletonMessageRenderer } from "../../lib/renderer/Singleton"; import { SingletonMessageRenderer } from "../../lib/renderer/Singleton";
...@@ -35,8 +34,6 @@ export interface ClientOperations { ...@@ -35,8 +34,6 @@ export interface ClientOperations {
logout: (shouldRequest?: boolean) => Promise<void>; logout: (shouldRequest?: boolean) => Promise<void>;
loggedIn: () => boolean; loggedIn: () => boolean;
ready: () => boolean; ready: () => boolean;
openDM: (user_id: string) => Promise<string>;
} }
// By the time they are used, they should all be initialized. // By the time they are used, they should all be initialized.
...@@ -52,7 +49,6 @@ type Props = { ...@@ -52,7 +49,6 @@ type Props = {
}; };
function Context({ auth, children }: Props) { function Context({ auth, children }: Props) {
const history = useHistory();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const [status, setStatus] = useState(ClientStatus.INIT); const [status, setStatus] = useState(ClientStatus.INIT);
const [client, setClient] = useState<Client>( const [client, setClient] = useState<Client>(
...@@ -61,34 +57,10 @@ function Context({ auth, children }: Props) { ...@@ -61,34 +57,10 @@ function Context({ auth, children }: Props) {
useEffect(() => { useEffect(() => {
(async () => { (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({ const client = new Client({
autoReconnect: false, autoReconnect: false,
apiURL: import.meta.env.VITE_API_URL, apiURL: import.meta.env.VITE_API_URL,
debug: import.meta.env.DEV, debug: import.meta.env.DEV,
db,
}); });
setClient(client); setClient(client);
...@@ -116,7 +88,7 @@ function Context({ auth, children }: Props) { ...@@ -116,7 +88,7 @@ function Context({ auth, children }: Props) {
if (onboarding) { if (onboarding) {
openScreen({ openScreen({
id: "onboarding", id: "onboarding",
callback: (username: string) => callback: async (username: string) =>
onboarding(username, true).then(login), onboarding(username, true).then(login),
}); });
} else { } else {
...@@ -149,25 +121,16 @@ function Context({ auth, children }: Props) { ...@@ -149,25 +121,16 @@ function Context({ auth, children }: Props) {
loggedIn: () => typeof auth.active !== "undefined", loggedIn: () => typeof auth.active !== "undefined",
ready: () => ready: () =>
operations.loggedIn() && typeof client.user !== "undefined", 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]); }, [client, auth.active, openScreen]);
useEffect( useEffect(
() => registerEvents({ operations }, setStatus, client), () => registerEvents({ operations }, setStatus, client),
[client], [client, operations],
); );
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (client.db) {
await client.restore();
}
if (auth.active) { if (auth.active) {
dispatch({ type: "QUEUE_FAIL_ALL" }); dispatch({ type: "QUEUE_FAIL_ALL" });
...@@ -216,6 +179,7 @@ function Context({ auth, children }: Props) { ...@@ -216,6 +179,7 @@ function Context({ auth, children }: Props) {
setStatus(ClientStatus.READY); setStatus(ClientStatus.READY);
} }
})(); })();
// eslint-disable-next-line
}, []); }, []);
if (status === ClientStatus.LOADING) { if (status === ClientStatus.LOADING) {
...@@ -239,3 +203,5 @@ export default connectState<{ children: Children }>(Context, (state) => { ...@@ -239,3 +203,5 @@ export default connectState<{ children: Children }>(Context, (state) => {
sync: state.sync, sync: state.sync,
}; };
}); });
export const useClient = () => useContext(AppContext);
/** /**
* This file monitors the message cache to delete any queued messages that have already sent. * This file monitors the message cache to delete any queued messages that have already sent.
*/ */
import { Message } from "revolt.js"; import { Message } from "revolt.js/dist/maps/Messages";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
import { dispatch } from "../../redux"; import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { QueuedMessage } from "../../redux/reducers/queue"; import { QueuedMessage } from "../../redux/reducers/queue";
import { Typing } from "../../redux/reducers/typing";
import { AppContext } from "./RevoltClient"; import { AppContext } from "./RevoltClient";
type Props = { type Props = {
messages: QueuedMessage[]; messages: QueuedMessage[];
typing: Typing;
}; };
function StateMonitor(props: Props) { function StateMonitor(props: Props) {
...@@ -39,31 +37,7 @@ function StateMonitor(props: Props) { ...@@ -39,31 +37,7 @@ function StateMonitor(props: Props) {
client.addListener("message", add); client.addListener("message", add);
return () => client.removeListener("message", add); return () => client.removeListener("message", add);
}, [props.messages]); }, [client, props.messages]);
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,
});
}
}
}
}
removeOld();
let interval = setInterval(removeOld, 1000);
return () => clearInterval(interval);
}, [props.typing]);
return null; return null;
} }
...@@ -71,6 +45,5 @@ function StateMonitor(props: Props) { ...@@ -71,6 +45,5 @@ function StateMonitor(props: Props) {
export default connectState(StateMonitor, (state) => { export default connectState(StateMonitor, (state) => {
return { return {
messages: [...state.queue], messages: [...state.queue],
typing: state.typing,
}; };
}); });
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
* This file monitors changes to settings and syncs them to the server. * This file monitors changes to settings and syncs them to the server.
*/ */
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { Sync } from "revolt.js/dist/api/objects"; import { UserSettings } from "revolt-api/types/Sync";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { useContext, useEffect } from "preact/hooks"; import { useCallback, useContext, useEffect, useMemo } from "preact/hooks";
import { dispatch } from "../../redux"; import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
...@@ -28,15 +28,15 @@ type Props = { ...@@ -28,15 +28,15 @@ type Props = {
notifications: Notifications; notifications: Notifications;
}; };
var lastValues: { [key in SyncKeys]?: any } = {}; const lastValues: { [key in SyncKeys]?: unknown } = {};
export function mapSync( export function mapSync(
packet: Sync.UserSettings, packet: UserSettings,
revision?: Record<string, number>, revision?: Record<string, number>,
) { ) {
let update: { [key in SyncKeys]?: [number, SyncData[key]] } = {}; const update: { [key in SyncKeys]?: [number, SyncData[key]] } = {};
for (let key of Object.keys(packet)) { for (const key of Object.keys(packet)) {
let [timestamp, obj] = packet[key]; const [timestamp, obj] = packet[key];
if (timestamp < (revision ?? {})[key] ?? 0) { if (timestamp < (revision ?? {})[key] ?? 0) {
continue; continue;
} }
...@@ -78,31 +78,38 @@ function SyncManager(props: Props) { ...@@ -78,31 +78,38 @@ function SyncManager(props: Props) {
.syncFetchUnreads() .syncFetchUnreads()
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads })); .then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
} }
}, [status]); }, [client, props.sync?.disabled, status]);
function syncChange(key: SyncKeys, data: any) { const syncChange = useCallback(
let timestamp = +new Date(); (key: SyncKeys, data: unknown) => {
dispatch({ const timestamp = +new Date();
type: "SYNC_SET_REVISION", dispatch({
key, type: "SYNC_SET_REVISION",
timestamp, key,
}); timestamp,
});
client.syncSetSettings(
{ client.syncSetSettings(
[key]: data, {
}, [key]: data as string,
timestamp, },
); timestamp,
} );
},
let disabled = props.sync.disabled ?? []; [client],
for (let [key, object] of [ );
const disabled = useMemo(
() => props.sync.disabled ?? [],
[props.sync.disabled],
);
for (const [key, object] of [
["appearance", props.settings.appearance], ["appearance", props.settings.appearance],
["theme", props.settings.theme], ["theme", props.settings.theme],
["locale", props.locale], ["locale", props.locale],
["notifications", props.notifications], ["notifications", props.notifications],
] as [SyncKeys, any][]) { ] as [SyncKeys, unknown][]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => { useEffect(() => {
if (disabled.indexOf(key) === -1) { if (disabled.indexOf(key) === -1) {
if (typeof lastValues[key] !== "undefined") { if (typeof lastValues[key] !== "undefined") {
...@@ -113,13 +120,13 @@ function SyncManager(props: Props) { ...@@ -113,13 +120,13 @@ function SyncManager(props: Props) {
} }
lastValues[key] = object; lastValues[key] = object;
}, [disabled, object]); }, [key, syncChange, disabled, object]);
} }
useEffect(() => { useEffect(() => {
function onPacket(packet: ClientboundNotification) { function onPacket(packet: ClientboundNotification) {
if (packet.type === "UserSettingsUpdate") { if (packet.type === "UserSettingsUpdate") {
let update: { [key in SyncKeys]?: [number, SyncData[key]] } = const update: { [key in SyncKeys]?: [number, SyncData[key]] } =
mapSync(packet.update, props.sync.revision); mapSync(packet.update, props.sync.revision);
dispatch({ dispatch({
...@@ -131,7 +138,7 @@ function SyncManager(props: Props) { ...@@ -131,7 +138,7 @@ function SyncManager(props: Props) {
client.addListener("packet", onPacket); client.addListener("packet", onPacket);
return () => client.removeListener("packet", onPacket); return () => client.removeListener("packet", onPacket);
}, [disabled, props.sync]); }, [client, disabled, props.sync]);
return null; return null;
} }
......
import { Client, Message } from "revolt.js/dist"; import { Client } from "revolt.js/dist";
import { Message } from "revolt.js/dist/maps/Messages";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { StateUpdater } from "preact/hooks"; import { StateUpdater } from "preact/hooks";
...@@ -7,7 +8,7 @@ import { dispatch } from "../../redux"; ...@@ -7,7 +8,7 @@ import { dispatch } from "../../redux";
import { ClientOperations, ClientStatus } from "./RevoltClient"; import { ClientOperations, ClientStatus } from "./RevoltClient";
export var preventReconnect = false; export let preventReconnect = false;
let preventUntil = 0; let preventUntil = 0;
export function setReconnectDisallowed(allowed: boolean) { export function setReconnectDisallowed(allowed: boolean) {
...@@ -33,6 +34,7 @@ export function registerEvents( ...@@ -33,6 +34,7 @@ export function registerEvents(
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let listeners: Record<string, (...args: any[]) => void> = { let listeners: Record<string, (...args: any[]) => void> = {
connecting: () => connecting: () =>
operations.ready() && setStatus(ClientStatus.CONNECTING), operations.ready() && setStatus(ClientStatus.CONNECTING),
...@@ -46,24 +48,6 @@ export function registerEvents( ...@@ -46,24 +48,6 @@ export function registerEvents(
packet: (packet: ClientboundNotification) => { packet: (packet: ClientboundNotification) => {
switch (packet.type) { 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": { case "ChannelAck": {
dispatch({ dispatch({
type: "UNREADS_MARK_READ", type: "UNREADS_MARK_READ",
...@@ -76,10 +60,10 @@ export function registerEvents( ...@@ -76,10 +60,10 @@ export function registerEvents(
}, },
message: (message: Message) => { message: (message: Message) => {
if (message.mentions?.includes(client.user!._id)) { if (message.mention_ids?.includes(client.user!._id)) {
dispatch({ dispatch({
type: "UNREADS_MENTION", type: "UNREADS_MENTION",
channel: message.channel, channel: message.channel_id,
message: message._id, message: message._id,
}); });
} }
...@@ -91,7 +75,7 @@ export function registerEvents( ...@@ -91,7 +75,7 @@ export function registerEvents(
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
listeners = new Proxy(listeners, { listeners = new Proxy(listeners, {
get: get:
(target, listener, receiver) => (target, listener) =>
(...args: unknown[]) => { (...args: unknown[]) => {
console.debug(`Calling ${listener.toString()} with`, args); console.debug(`Calling ${listener.toString()} with`, args);
Reflect.get(target, listener)(...args); Reflect.get(target, listener)(...args);
...@@ -104,17 +88,6 @@ export function registerEvents( ...@@ -104,17 +88,6 @@ export function registerEvents(
client.addListener(listener, listeners[listener]); client.addListener(listener, listeners[listener]);
} }
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);
}
const online = () => { const online = () => {
if (operations.ready()) { if (operations.ready()) {
setStatus(ClientStatus.RECONNECTING); setStatus(ClientStatus.RECONNECTING);
...@@ -142,13 +115,6 @@ export function registerEvents( ...@@ -142,13 +115,6 @@ export function registerEvents(
); );
} }
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("online", online);
window.removeEventListener("offline", offline); window.removeEventListener("offline", offline);
}; };
......
import { Client, PermissionCalculator } from "revolt.js";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
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;
}
export function useForceUpdate(context?: HookContext): HookContext {
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 = () => {};
}
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]
>;
// 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
>;
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();
}
}
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();
}
export function useUser(id?: string, context?: HookContext) {
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);
}
export function useUsers(ids?: string[], context?: HookContext) {
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;
}
export function useChannels(ids?: string[], context?: HookContext) {
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;
}
export function useServers(ids?: string[], context?: HookContext) {
return useObject("servers", ids, context) as (
| Readonly<Servers.Server>
| undefined
)[];
}
export function useDMs(context?: HookContext) {
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();
}
}
}
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
)[];
}
export function useUserPermission(id: string, context?: HookContext) {
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);
}
export function useChannelPermission(id: string, context?: HookContext) {
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 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);
if (server) {
ctx.client.servers.addListener("update", mutationServer);
ctx.client.servers.members.addListener("update", mutationMember);
}
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);
}
export function useServerPermission(id: string, context?: HookContext) {
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();
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);
}
import { Client } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Channel, Message, User } from "revolt.js/dist/api/objects";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function takeError(error: any): string { export function takeError(error: any): string {
const type = error?.response?.data?.type; const type = error?.response?.data?.type;
let id = type; const id = type;
if (!type) { if (!type) {
if (error?.response?.status === 403) { if (error?.response?.status === 403) {
return "Unauthorized"; return "Unauthorized";
...@@ -23,7 +23,6 @@ export function takeError(error: any): string { ...@@ -23,7 +23,6 @@ export function takeError(error: any): string {
} }
export function getChannelName( export function getChannelName(
client: Client,
channel: Channel, channel: Channel,
prefixType?: boolean, prefixType?: boolean,
): Children { ): Children {
...@@ -31,11 +30,10 @@ export function getChannelName( ...@@ -31,11 +30,10 @@ export function getChannelName(
return <Text id="app.navigation.tabs.saved" />; return <Text id="app.navigation.tabs.saved" />;
if (channel.channel_type === "DirectMessage") { if (channel.channel_type === "DirectMessage") {
let uid = client.channels.getRecipient(channel._id);
return ( return (
<> <>
{prefixType && "@"} {prefixType && "@"}
{client.users.get(uid)?.username} {channel.recipient!.username}
</> </>
); );
} }
...@@ -46,12 +44,3 @@ export function getChannelName( ...@@ -46,12 +44,3 @@ export function getChannelName(
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;
}
type Build = "stable" | "nightly" | "dev";
type NativeConfig = {
frame: boolean;
build: Build;
discordRPC: boolean;
hardwareAcceleration: boolean;
};
declare interface Window {
isNative?: boolean;
nativeVersion: string;
native: {
min();
max();
close();
reload();
relaunch();
getConfig(): NativeConfig;
set(key: keyof NativeConfig, value: unknown);
getAutoStart(): Promise<boolean>;
enableAutoStart(): Promise<void>;
disableAutoStart(): Promise<void>;
};
}
declare const Fragment = preact.Fragment;
...@@ -10,7 +10,6 @@ export default function ConditionalLink(props: Props) { ...@@ -10,7 +10,6 @@ export default function ConditionalLink(props: Props) {
if (active) { if (active) {
return <a>{props.children}</a>; return <a>{props.children}</a>;
} else {
return <Link {...linkProps} />;
} }
return <Link {...linkProps} />;
} }
...@@ -10,23 +10,21 @@ import { ...@@ -10,23 +10,21 @@ import {
LeftArrowAlt, LeftArrowAlt,
Trash, Trash,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog, UserVoice } from "@styled-icons/boxicons-solid";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import { Attachment } from "revolt-api/types/Autumn";
Attachment, import { Presence, RelationshipStatus } from "revolt-api/types/Users";
Channels,
Message,
Servers,
Users,
} from "revolt.js/dist/api/objects";
import { import {
ChannelPermission, ChannelPermission,
ServerPermission, ServerPermission,
UserPermission, UserPermission,
} from "revolt.js/dist/api/permissions"; } from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { import {
ContextMenu,
ContextMenuWithData, ContextMenuWithData,
MenuItem, MenuItem,
openContextMenu, openContextMenu,
...@@ -43,23 +41,15 @@ import { ...@@ -43,23 +41,15 @@ import {
} from "../redux/reducers/notifications"; } from "../redux/reducers/notifications";
import { QueuedMessage } from "../redux/reducers/queue"; import { QueuedMessage } from "../redux/reducers/queue";
import { useIntermediate } from "../context/intermediate/Intermediate"; import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
import { import {
AppContext, AppContext,
ClientStatus, ClientStatus,
StatusContext, StatusContext,
} from "../context/revoltjs/RevoltClient"; } from "../context/revoltjs/RevoltClient";
import {
useChannel,
useChannelPermission,
useForceUpdate,
useServer,
useServerPermission,
useUser,
useUserPermission,
} from "../context/revoltjs/hooks";
import { takeError } from "../context/revoltjs/util"; import { takeError } from "../context/revoltjs/util";
import Tooltip from "../components/common/Tooltip";
import UserStatus from "../components/common/user/UserStatus"; import UserStatus from "../components/common/user/UserStatus";
import IconButton from "../components/ui/IconButton"; import IconButton from "../components/ui/IconButton";
import LineDivider from "../components/ui/LineDivider"; import LineDivider from "../components/ui/LineDivider";
...@@ -83,49 +73,46 @@ type Action = ...@@ -83,49 +73,46 @@ type Action =
| { action: "copy_id"; id: string } | { action: "copy_id"; id: string }
| { action: "copy_selection" } | { action: "copy_selection" }
| { action: "copy_text"; content: string } | { action: "copy_text"; content: string }
| { action: "mark_as_read"; channel: Channels.Channel } | { action: "mark_as_read"; channel: Channel }
| { action: "retry_message"; message: QueuedMessage } | { action: "retry_message"; message: QueuedMessage }
| { action: "cancel_message"; message: QueuedMessage } | { action: "cancel_message"; message: QueuedMessage }
| { action: "mention"; user: string } | { action: "mention"; user: string }
| { action: "reply_message"; id: string } | { action: "reply_message"; id: string }
| { action: "quote_message"; content: string } | { action: "quote_message"; content: string }
| { action: "edit_message"; id: string } | { action: "edit_message"; id: string }
| { action: "delete_message"; target: Channels.Message } | { action: "delete_message"; target: Message }
| { action: "open_file"; attachment: Attachment } | { action: "open_file"; attachment: Attachment }
| { action: "save_file"; attachment: Attachment } | { action: "save_file"; attachment: Attachment }
| { action: "copy_file_link"; attachment: Attachment } | { action: "copy_file_link"; attachment: Attachment }
| { action: "open_link"; link: string } | { action: "open_link"; link: string }
| { action: "copy_link"; link: string } | { action: "copy_link"; link: string }
| { action: "remove_member"; channel: string; user: string } | { action: "remove_member"; channel: Channel; user: User }
| { action: "kick_member"; target: Servers.Server; user: string } | { action: "kick_member"; target: Server; user: User }
| { action: "ban_member"; target: Servers.Server; user: string } | { action: "ban_member"; target: Server; user: User }
| { action: "view_profile"; user: string } | { action: "view_profile"; user: User }
| { action: "message_user"; user: string } | { action: "message_user"; user: User }
| { action: "block_user"; user: Users.User } | { action: "block_user"; user: User }
| { action: "unblock_user"; user: Users.User } | { action: "unblock_user"; user: User }
| { action: "add_friend"; user: Users.User } | { action: "add_friend"; user: User }
| { action: "remove_friend"; user: Users.User } | { action: "remove_friend"; user: User }
| { action: "cancel_friend"; user: Users.User } | { action: "cancel_friend"; user: User }
| { action: "set_presence"; presence: Users.Presence } | { action: "set_presence"; presence: Presence }
| { action: "set_status" } | { action: "set_status" }
| { action: "clear_status" } | { action: "clear_status" }
| { action: "create_channel"; target: Servers.Server } | { action: "create_channel"; target: Server }
| { | {
action: "create_invite"; action: "create_invite";
target: target: Channel;
| Channels.GroupChannel
| Channels.TextChannel
| Channels.VoiceChannel;
} }
| { action: "leave_group"; target: Channels.GroupChannel } | { action: "leave_group"; target: Channel }
| { | {
action: "delete_channel"; action: "delete_channel";
target: Channels.TextChannel | Channels.VoiceChannel; target: Channel;
} }
| { action: "close_dm"; target: Channels.DirectMessageChannel } | { action: "close_dm"; target: Channel }
| { action: "leave_server"; target: Servers.Server } | { action: "leave_server"; target: Server }
| { action: "delete_server"; target: Servers.Server } | { action: "delete_server"; target: Server }
| { action: "open_notification_options"; channel: Channels.Channel } | { action: "open_notification_options"; channel: Channel }
| { action: "open_settings" } | { action: "open_settings" }
| { action: "open_channel_settings"; id: string } | { action: "open_channel_settings"; id: string }
| { action: "open_server_settings"; id: string } | { action: "open_server_settings"; id: string }
...@@ -140,6 +127,8 @@ type Props = { ...@@ -140,6 +127,8 @@ type Props = {
notifications: Notifications; notifications: Notifications;
}; };
// ! FIXME: I dare someone to re-write this
// Tip: This should just be split into separate context menus per logical area.
function ContextMenus(props: Props) { function ContextMenus(props: Props) {
const { openScreen, writeClipboard } = useIntermediate(); const { openScreen, writeClipboard } = useIntermediate();
const client = useContext(AppContext); const client = useContext(AppContext);
...@@ -167,10 +156,11 @@ function ContextMenus(props: Props) { ...@@ -167,10 +156,11 @@ function ContextMenus(props: Props) {
) )
return; return;
let message = const message =
data.channel.channel_type === "TextChannel" typeof data.channel.last_message === "string"
? data.channel.last_message ? data.channel.last_message
: data.channel.last_message._id; : data.channel.last_message!._id;
dispatch({ dispatch({
type: "UNREADS_MARK_READ", type: "UNREADS_MARK_READ",
channel: data.channel._id, channel: data.channel._id,
...@@ -187,7 +177,7 @@ function ContextMenus(props: Props) { ...@@ -187,7 +177,7 @@ function ContextMenus(props: Props) {
case "retry_message": case "retry_message":
{ {
const nonce = data.message.id; const nonce = data.message.id;
const fail = (error: any) => const fail = (error: string) =>
dispatch({ dispatch({
type: "QUEUE_FAIL", type: "QUEUE_FAIL",
nonce, nonce,
...@@ -195,7 +185,8 @@ function ContextMenus(props: Props) { ...@@ -195,7 +185,8 @@ function ContextMenus(props: Props) {
}); });
client.channels client.channels
.sendMessage(data.message.channel, { .get(data.message.channel)!
.sendMessage({
nonce: data.message.id, nonce: data.message.id,
content: data.message.data.content as string, content: data.message.data.content as string,
replies: data.message.data.replies, replies: data.message.data.replies,
...@@ -291,8 +282,9 @@ function ContextMenus(props: Props) { ...@@ -291,8 +282,9 @@ function ContextMenus(props: Props) {
const { filename } = data.attachment; const { filename } = data.attachment;
writeClipboard( writeClipboard(
// ! FIXME: do from r.js // ! FIXME: do from r.js
client.generateFileURL(data.attachment) + `${client.generateFileURL(
`/${encodeURI(filename)}`, data.attachment,
)}/${encodeURI(filename)}`,
); );
} }
break; break;
...@@ -311,17 +303,17 @@ function ContextMenus(props: Props) { ...@@ -311,17 +303,17 @@ function ContextMenus(props: Props) {
case "remove_member": case "remove_member":
{ {
client.channels.removeMember(data.channel, data.user); data.channel.removeMember(data.user._id);
} }
break; break;
case "view_profile": case "view_profile":
openScreen({ id: "profile", user_id: data.user }); openScreen({ id: "profile", user_id: data.user._id });
break; break;
case "message_user": case "message_user":
{ {
const channel = await client.users.openDM(data.user); const channel = await data.user.openDM();
if (channel) { if (channel) {
history.push(`/channel/${channel._id}`); history.push(`/channel/${channel._id}`);
} }
...@@ -330,7 +322,7 @@ function ContextMenus(props: Props) { ...@@ -330,7 +322,7 @@ function ContextMenus(props: Props) {
case "add_friend": case "add_friend":
{ {
await client.users.addFriend(data.user.username); await data.user.addFriend();
} }
break; break;
...@@ -342,7 +334,7 @@ function ContextMenus(props: Props) { ...@@ -342,7 +334,7 @@ function ContextMenus(props: Props) {
}); });
break; break;
case "unblock_user": case "unblock_user":
await client.users.unblockUser(data.user._id); await data.user.unblockUser();
break; break;
case "remove_friend": case "remove_friend":
openScreen({ openScreen({
...@@ -352,12 +344,12 @@ function ContextMenus(props: Props) { ...@@ -352,12 +344,12 @@ function ContextMenus(props: Props) {
}); });
break; break;
case "cancel_friend": case "cancel_friend":
await client.users.removeFriend(data.user._id); await data.user.removeFriend();
break; break;
case "set_presence": case "set_presence":
{ {
await client.users.editUser({ await client.users.edit({
status: { status: {
...client.user?.status, ...client.user?.status,
presence: data.presence, presence: data.presence,
...@@ -375,8 +367,9 @@ function ContextMenus(props: Props) { ...@@ -375,8 +367,9 @@ function ContextMenus(props: Props) {
case "clear_status": case "clear_status":
{ {
let { text, ...status } = client.user?.status ?? {}; const { text: _text, ...status } =
await client.users.editUser({ status }); client.user?.status ?? {};
await client.users.edit({ status });
} }
break; break;
...@@ -388,12 +381,12 @@ function ContextMenus(props: Props) { ...@@ -388,12 +381,12 @@ function ContextMenus(props: Props) {
case "delete_message": case "delete_message":
case "create_channel": case "create_channel":
case "create_invite": case "create_invite":
// The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever // Typescript flattens the case types into a single type and type structure and specifity is lost
openScreen({ openScreen({
id: "special_prompt", id: "special_prompt",
type: data.action, type: data.action,
target: data.target as any, target: data.target,
}); } as unknown as Screen);
break; break;
case "ban_member": case "ban_member":
...@@ -456,9 +449,8 @@ function ContextMenus(props: Props) { ...@@ -456,9 +449,8 @@ function ContextMenus(props: Props) {
unread, unread,
contextualChannel: cxid, contextualChannel: cxid,
}: ContextMenuData) => { }: ContextMenuData) => {
const forceUpdate = useForceUpdate();
const elements: Children[] = []; const elements: Children[] = [];
var lastDivider = false; let lastDivider = false;
function generateAction( function generateAction(
action: Action, action: Action,
...@@ -486,11 +478,8 @@ function ContextMenus(props: Props) { ...@@ -486,11 +478,8 @@ function ContextMenus(props: Props) {
} }
if (server_list) { if (server_list) {
let server = useServer(server_list, forceUpdate); const server = client.servers.get(server_list)!;
let permissions = useServerPermission( const permissions = server.permission;
server_list,
forceUpdate,
);
if (server) { if (server) {
if (permissions & ServerPermission.ManageChannels) if (permissions & ServerPermission.ManageChannels)
generateAction({ generateAction({
...@@ -517,33 +506,31 @@ function ContextMenus(props: Props) { ...@@ -517,33 +506,31 @@ function ContextMenus(props: Props) {
pushDivider(); pushDivider();
} }
const channel = useChannel(cid, forceUpdate); const channel = cid ? client.channels.get(cid) : undefined;
const contextualChannel = useChannel(cxid, forceUpdate); const contextualChannel = cxid
? client.channels.get(cxid)
: undefined;
const targetChannel = channel ?? contextualChannel; const targetChannel = channel ?? contextualChannel;
const user = useUser(uid, forceUpdate); const user = uid ? client.users.get(uid) : undefined;
const serverChannel = const serverChannel =
targetChannel && targetChannel &&
(targetChannel.channel_type === "TextChannel" || (targetChannel.channel_type === "TextChannel" ||
targetChannel.channel_type === "VoiceChannel") targetChannel.channel_type === "VoiceChannel")
? targetChannel ? targetChannel
: undefined; : undefined;
const server = useServer(
serverChannel ? serverChannel.server : sid,
forceUpdate,
);
const channelPermissions = targetChannel const s = serverChannel ? serverChannel.server_id! : sid;
? useChannelPermission(targetChannel._id, forceUpdate) const server = s ? client.servers.get(s) : undefined;
: 0;
const serverPermissions = server const channelPermissions = targetChannel?.permission || 0;
? useServerPermission(server._id, forceUpdate) const serverPermissions =
: serverChannel (server
? useServerPermission(serverChannel.server, forceUpdate) ? server.permission
: 0; : serverChannel
const userPermissions = user ? serverChannel.server?.permission
? useUserPermission(user._id, forceUpdate) : 0) || 0;
: 0; const userPermissions = (user ? user.permission : 0) || 0;
if (channel && unread) { if (channel && unread) {
generateAction({ action: "mark_as_read", channel }); generateAction({ action: "mark_as_read", channel });
...@@ -563,29 +550,29 @@ function ContextMenus(props: Props) { ...@@ -563,29 +550,29 @@ function ContextMenus(props: Props) {
if (user) { if (user) {
let actions: Action["action"][]; let actions: Action["action"][];
switch (user.relationship) { switch (user.relationship) {
case Users.Relationship.User: case RelationshipStatus.User:
actions = []; actions = [];
break; break;
case Users.Relationship.Friend: case RelationshipStatus.Friend:
actions = ["remove_friend", "block_user"]; actions = ["remove_friend", "block_user"];
break; break;
case Users.Relationship.Incoming: case RelationshipStatus.Incoming:
actions = [ actions = [
"add_friend", "add_friend",
"cancel_friend", "cancel_friend",
"block_user", "block_user",
]; ];
break; break;
case Users.Relationship.Outgoing: case RelationshipStatus.Outgoing:
actions = ["cancel_friend", "block_user"]; actions = ["cancel_friend", "block_user"];
break; break;
case Users.Relationship.Blocked: case RelationshipStatus.Blocked:
actions = ["unblock_user"]; actions = ["unblock_user"];
break; break;
case Users.Relationship.BlockedOther: case RelationshipStatus.BlockedOther:
actions = ["block_user"]; actions = ["block_user"];
break; break;
case Users.Relationship.None: case RelationshipStatus.None:
default: default:
actions = ["add_friend", "block_user"]; actions = ["add_friend", "block_user"];
} }
...@@ -593,7 +580,7 @@ function ContextMenus(props: Props) { ...@@ -593,7 +580,7 @@ function ContextMenus(props: Props) {
if (userPermissions & UserPermission.ViewProfile) { if (userPermissions & UserPermission.ViewProfile) {
generateAction({ generateAction({
action: "view_profile", action: "view_profile",
user: user._id, user,
}); });
} }
...@@ -603,26 +590,29 @@ function ContextMenus(props: Props) { ...@@ -603,26 +590,29 @@ function ContextMenus(props: Props) {
) { ) {
generateAction({ generateAction({
action: "message_user", action: "message_user",
user: user._id, user,
}); });
} }
for (let i = 0; i < actions.length; i++) { for (let i = 0; i < actions.length; i++) {
// The any here is because typescript can't determine that user the actions are linked together correctly // Typescript can't determine that user the actions are linked together correctly
generateAction({ action: actions[i] as any, user }); generateAction({
action: actions[i],
user,
} as unknown as Action);
} }
} }
if (contextualChannel) { if (contextualChannel) {
if (contextualChannel.channel_type === "Group" && uid) { if (contextualChannel.channel_type === "Group" && uid) {
if ( if (
contextualChannel.owner === userId && contextualChannel.owner_id === userId &&
userId !== uid userId !== uid
) { ) {
generateAction({ generateAction({
action: "remove_member", action: "remove_member",
channel: contextualChannel._id, channel: contextualChannel,
user: uid, user: user!,
}); });
} }
} }
...@@ -639,14 +629,14 @@ function ContextMenus(props: Props) { ...@@ -639,14 +629,14 @@ function ContextMenus(props: Props) {
generateAction({ generateAction({
action: "kick_member", action: "kick_member",
target: server, target: server,
user: uid, user: user!,
}); });
if (serverPermissions & ServerPermission.BanMembers) if (serverPermissions & ServerPermission.BanMembers)
generateAction({ generateAction({
action: "ban_member", action: "ban_member",
target: server, target: server,
user: uid, user: user!,
}); });
} }
} }
...@@ -684,7 +674,7 @@ function ContextMenus(props: Props) { ...@@ -684,7 +674,7 @@ function ContextMenus(props: Props) {
}); });
} }
if (message.author === userId) { if (message.author_id === userId) {
generateAction({ generateAction({
action: "edit_message", action: "edit_message",
id: message._id, id: message._id,
...@@ -692,7 +682,7 @@ function ContextMenus(props: Props) { ...@@ -692,7 +682,7 @@ function ContextMenus(props: Props) {
} }
if ( if (
message.author === userId || message.author_id === userId ||
channelPermissions & channelPermissions &
ChannelPermission.ManageMessages ChannelPermission.ManageMessages
) { ) {
...@@ -741,7 +731,7 @@ function ContextMenus(props: Props) { ...@@ -741,7 +731,7 @@ function ContextMenus(props: Props) {
} }
if (document.activeElement?.tagName === "A") { if (document.activeElement?.tagName === "A") {
let link = const link =
document.activeElement.getAttribute("href"); document.activeElement.getAttribute("href");
if (link) { if (link) {
pushDivider(); pushDivider();
...@@ -751,7 +741,7 @@ function ContextMenus(props: Props) { ...@@ -751,7 +741,7 @@ function ContextMenus(props: Props) {
} }
} }
let id = sid ?? cid ?? uid ?? message?._id; const id = sid ?? cid ?? uid ?? message?._id;
if (id) { if (id) {
pushDivider(); pushDivider();
...@@ -794,11 +784,15 @@ function ContextMenus(props: Props) { ...@@ -794,11 +784,15 @@ function ContextMenus(props: Props) {
break; break;
case "TextChannel": case "TextChannel":
case "VoiceChannel": case "VoiceChannel":
// ! FIXME: add permission for invites if (
generateAction({ channelPermissions &
action: "create_invite", ChannelPermission.InviteOthers
target: channel, ) {
}); generateAction({
action: "create_invite",
target: channel,
});
}
if ( if (
serverPermissions & serverPermissions &
...@@ -807,7 +801,7 @@ function ContextMenus(props: Props) { ...@@ -807,7 +801,7 @@ function ContextMenus(props: Props) {
generateAction( generateAction(
{ {
action: "open_server_channel_settings", action: "open_server_channel_settings",
server: channel.server, server: channel.server_id!,
id: channel._id, id: channel._id,
}, },
"open_channel_settings", "open_channel_settings",
...@@ -871,92 +865,112 @@ function ContextMenus(props: Props) { ...@@ -871,92 +865,112 @@ function ContextMenus(props: Props) {
id="Status" id="Status"
onClose={contextClick} onClose={contextClick}
className="Status"> className="Status">
{() => ( {() => {
<> const user = client.user!;
<div className="header"> return (
<div className="main"> <>
<div>@{client.user!.username}</div> <div className="header">
<div className="status"> <div className="main">
<UserStatus user={client.user!} /> <div
className="username"
onClick={() =>
writeClipboard(
client.user!.username,
)
}>
<Tooltip
content={
<Text id="app.special.copy_username" />
}>
@{user.username}
</Tooltip>
</div>
<div
className="status"
onClick={() =>
contextClick({
action: "set_status",
})
}>
<UserStatus user={user} />
</div>
</div> </div>
</div>
<IconButton>
<MenuItem data={{ action: "open_settings" }}>
<Cog size={18} />
</MenuItem>
</IconButton>
</div>
<LineDivider />
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Online,
}}
disabled={!isOnline}>
<div className="indicator online" />
<Text id={`app.status.online`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Idle,
}}
disabled={!isOnline}>
<div className="indicator idle" />
<Text id={`app.status.idle`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Busy,
}}
disabled={!isOnline}>
<div className="indicator busy" />
<Text id={`app.status.busy`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Users.Presence.Invisible,
}}
disabled={!isOnline}>
<div className="indicator invisible" />
<Text id={`app.status.invisible`} />
</MenuItem>
<LineDivider />
<div className="header">
<div className="main">
<MenuItem
data={{ action: "set_status" }}
disabled={!isOnline}>
<Text
id={`app.context_menu.custom_status`}
/>
</MenuItem>
</div>
{client.user!.status?.text && (
<IconButton> <IconButton>
<MenuItem data={{ action: "clear_status" }}> <MenuItem
<Trash size={18} /> data={{ action: "open_settings" }}>
<Cog size={22} />
</MenuItem> </MenuItem>
</IconButton> </IconButton>
)} </div>
</div> <LineDivider />
</> <MenuItem
)} data={{
action: "set_presence",
presence: Presence.Online,
}}
disabled={!isOnline}>
<div className="indicator online" />
<Text id={`app.status.online`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Presence.Idle,
}}
disabled={!isOnline}>
<div className="indicator idle" />
<Text id={`app.status.idle`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Presence.Busy,
}}
disabled={!isOnline}>
<div className="indicator busy" />
<Text id={`app.status.busy`} />
</MenuItem>
<MenuItem
data={{
action: "set_presence",
presence: Presence.Invisible,
}}
disabled={!isOnline}>
<div className="indicator invisible" />
<Text id={`app.status.invisible`} />
</MenuItem>
<LineDivider />
<MenuItem
data={{ action: "set_status" }}
disabled={!isOnline}>
<UserVoice size={18} />
<Text id={`app.context_menu.custom_status`} />
{client.user!.status?.text && (
<IconButton>
<MenuItem
data={{ action: "clear_status" }}>
<Trash size={18} />
</MenuItem>
</IconButton>
)}
</MenuItem>
</>
);
}}
</ContextMenuWithData> </ContextMenuWithData>
<ContextMenuWithData <ContextMenuWithData
id="NotificationOptions" id="NotificationOptions"
onClose={contextClick}> onClose={contextClick}>
{({ channel }: { channel: Channels.Channel }) => { {({ channel }: { channel: Channel }) => {
const state = props.notifications[channel._id]; const state = props.notifications[channel._id];
const actual = getNotificationState( const actual = getNotificationState(
props.notifications, props.notifications,
channel, channel,
); );
let elements: Children[] = [ const elements: Children[] = [
<MenuItem <MenuItem
key="notif"
data={{ data={{
action: "set_notification_state", action: "set_notification_state",
key: channel._id, key: channel._id,
...@@ -976,6 +990,7 @@ function ContextMenus(props: Props) { ...@@ -976,6 +990,7 @@ function ContextMenus(props: Props) {
function generate(key: string, icon: Children) { function generate(key: string, icon: Children) {
elements.push( elements.push(
<MenuItem <MenuItem
key={key}
data={{ data={{
action: "set_notification_state", action: "set_notification_state",
key: channel._id, key: channel._id,
......
/* eslint-disable react-hooks/rules-of-hooks */
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
const counts: { [key: string]: number } = {}; const counts: { [key: string]: number } = {};
...@@ -11,7 +12,7 @@ export default function PaintCounter({ ...@@ -11,7 +12,7 @@ export default function PaintCounter({
}) { }) {
if (import.meta.env.PROD && !always) return null; if (import.meta.env.PROD && !always) return null;
const [uniqueId] = useState("" + Math.random()); const [uniqueId] = useState(`${Math.random()}`);
const count = counts[uniqueId] ?? 0; const count = counts[uniqueId] ?? 0;
counts[uniqueId] = count + 1; counts[uniqueId] = count + 1;
return ( return (
......
import styled from "styled-components"; import styled from "styled-components";
import { RefObject } from "preact";
import { useEffect, useLayoutEffect, useRef } from "preact/hooks"; import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
import TextArea, { import TextArea, { TextAreaProps } from "../components/ui/TextArea";
DEFAULT_LINE_HEIGHT,
DEFAULT_TEXT_AREA_PADDING,
TextAreaProps,
TEXT_AREA_BORDER_WIDTH,
} from "../components/ui/TextArea";
import { internalSubscribe } from "./eventEmitter"; import { internalSubscribe } from "./eventEmitter";
import { isTouchscreenDevice } from "./isTouchscreenDevice"; import { isTouchscreenDevice } from "./isTouchscreenDevice";
type TextAreaAutoSizeProps = Omit< type TextAreaAutoSizeProps = Omit<
JSX.HTMLAttributes<HTMLTextAreaElement>, JSX.HTMLAttributes<HTMLTextAreaElement>,
"style" | "value" | "onChange" "style" | "value" | "onChange" | "children" | "as"
> & > &
TextAreaProps & { TextAreaProps & {
forceFocus?: boolean; forceFocus?: boolean;
...@@ -34,7 +30,7 @@ const Container = styled.div` ...@@ -34,7 +30,7 @@ const Container = styled.div`
flex-direction: column; flex-direction: column;
`; `;
const Ghost = styled.div<{ lineHeight: string, maxRows: number }>` const Ghost = styled.div<{ lineHeight: string; maxRows: number }>`
flex: 0; flex: 0;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
...@@ -45,13 +41,15 @@ const Ghost = styled.div<{ lineHeight: string, maxRows: number }>` ...@@ -45,13 +41,15 @@ const Ghost = styled.div<{ lineHeight: string, maxRows: number }>`
width: 100%; width: 100%;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
top: 0; top: 0;
position: absolute; position: absolute;
font-size: var(--text-size); font-size: var(--text-size);
line-height: ${(props) => props.lineHeight}; line-height: ${(props) => props.lineHeight};
max-height: calc(calc( ${(props) => props.lineHeight} * ${ (props) => props.maxRows } )); max-height: calc(
calc(${(props) => props.lineHeight} * ${(props) => props.maxRows})
);
} }
`; `;
...@@ -65,28 +63,29 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -65,28 +63,29 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
lineHeight, lineHeight,
hideBorder, hideBorder,
forceFocus, forceFocus,
children,
as,
onChange, onChange,
...textAreaProps ...textAreaProps
} = props; } = props;
const ref = useRef<HTMLTextAreaElement>(); const ref = useRef<HTMLTextAreaElement>() as RefObject<HTMLTextAreaElement>;
const ghost = useRef<HTMLDivElement>(); const ghost = useRef<HTMLDivElement>() as RefObject<HTMLDivElement>;
useLayoutEffect(() => { useLayoutEffect(() => {
ref.current.style.height = ghost.current.clientHeight + 'px'; if (ref.current && ghost.current) {
ref.current.style.height = `${ghost.current.clientHeight}px`;
}
}, [ghost, props.value]); }, [ghost, props.value]);
useEffect(() => { useEffect(() => {
if (isTouchscreenDevice) return; if (isTouchscreenDevice) return;
autoFocus && ref.current.focus(); autoFocus && ref.current && ref.current.focus();
}, [value]); }, [value, autoFocus]);
const inputSelected = () => const inputSelected = () =>
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? ""); ["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
useEffect(() => { useEffect(() => {
if (!ref.current) return;
if (forceFocus) { if (forceFocus) {
ref.current.focus(); ref.current.focus();
} }
...@@ -107,23 +106,28 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -107,23 +106,28 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return; if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
if (e.key.length !== 1) return; if (e.key.length !== 1) return;
if (ref && !inputSelected()) { if (ref && !inputSelected()) {
ref.current.focus(); ref.current!.focus();
} }
} }
document.body.addEventListener("keydown", keyDown); document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown); return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]); }, [ref, autoFocus, forceFocus, value]);
useEffect(() => { useEffect(() => {
if (!ref.current) return;
function focus(id: string) { function focus(id: string) {
if (id === props.id) { if (id === props.id) {
ref.current.focus(); ref.current!.focus();
} }
} }
return internalSubscribe("TextArea", "focus", focus); return internalSubscribe(
}, [ref]); "TextArea",
"focus",
focus as (...args: unknown[]) => void,
);
}, [props.id, ref]);
return ( return (
<Container> <Container>
...@@ -131,7 +135,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -131,7 +135,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
ref={ref} ref={ref}
value={value} value={value}
padding={padding} padding={padding}
style={{ height: minHeight }} style={{ minHeight }}
hideBorder={hideBorder} hideBorder={hideBorder}
lineHeight={lineHeight} lineHeight={lineHeight}
onChange={(ev) => { onChange={(ev) => {
...@@ -139,7 +143,9 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { ...@@ -139,7 +143,9 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
}} }}
{...textAreaProps} {...textAreaProps}
/> />
<Ghost lineHeight={lineHeight ?? 'var(--textarea-line-height)'} maxRows={maxRows ?? 5}> <Ghost
lineHeight={lineHeight ?? "var(--textarea-line-height)"}
maxRows={maxRows ?? 5}>
<div ref={ghost} style={{ padding }}> <div ref={ghost} style={{ padding }}>
{props.value {props.value
? props.value ? props.value
......
export function urlBase64ToUint8Array(base64String: string) { export function urlBase64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding) const base64 = (base64String + padding)
.replace(/\-/g, "+") .replace(/-/g, "+")
.replace(/_/g, "/"); .replace(/_/g, "/");
const rawData = window.atob(base64); const rawData = window.atob(base64);
......
export function debounce(cb: Function, duration: number) { export function debounce(cb: (...args: unknown[]) => void, duration: number) {
// Store the timer variable. // Store the timer variable.
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
// This function is given to React. // This function is given to React.
return (...args: any[]) => { return (...args: unknown[]) => {
// Get rid of the old timer. // Get rid of the old timer.
clearTimeout(timer); clearTimeout(timer);
// Set a new timer. // Set a new timer.
......
...@@ -5,19 +5,20 @@ export const InternalEvent = new EventEmitter(); ...@@ -5,19 +5,20 @@ export const InternalEvent = new EventEmitter();
export function internalSubscribe( export function internalSubscribe(
ns: string, ns: string,
event: string, event: string,
fn: (...args: any[]) => void, fn: (...args: unknown[]) => void,
) { ) {
InternalEvent.addListener(ns + "/" + event, fn); InternalEvent.addListener(`${ns}/${event}`, fn);
return () => InternalEvent.removeListener(ns + "/" + event, fn); return () => InternalEvent.removeListener(`${ns}/${event}`, fn);
} }
export function internalEmit(ns: string, event: string, ...args: any[]) { export function internalEmit(ns: string, event: string, ...args: unknown[]) {
InternalEvent.emit(ns + "/" + event, ...args); InternalEvent.emit(`${ns}/${event}`, ...args);
} }
// Event structure: namespace/event // Event structure: namespace/event
/// Event List /// Event List
// - MessageArea/jump_to_bottom
// - MessageRenderer/edit_last // - MessageRenderer/edit_last
// - MessageRenderer/edit_message // - MessageRenderer/edit_message
// - Intermediate/open_profile // - Intermediate/open_profile
...@@ -25,4 +26,5 @@ export function internalEmit(ns: string, event: string, ...args: any[]) { ...@@ -25,4 +26,5 @@ export function internalEmit(ns: string, event: string, ...args: any[]) {
// - MessageBox/append // - MessageBox/append
// - TextArea/focus // - TextArea/focus
// - ReplyBar/add // - ReplyBar/add
// - Modal/close
// - PWA/update // - PWA/update
import { IntlContext, translate } from "preact-i18n"; import { IntlContext, translate } from "preact-i18n";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { Dictionary } from "../context/Locale";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
interface Fields { interface Fields {
...@@ -14,9 +16,7 @@ interface Props { ...@@ -14,9 +16,7 @@ interface Props {
export interface IntlType { export interface IntlType {
intl: { intl: {
dictionary: { dictionary: Dictionary;
[key: string]: Object | string;
};
}; };
} }
...@@ -36,10 +36,9 @@ function recursiveReplaceFields(input: string, fields: Fields) { ...@@ -36,10 +36,9 @@ function recursiveReplaceFields(input: string, fields: Fields) {
} }
return values.flat(); return values.flat();
} else {
// base case
return [input];
} }
// base case
return [input];
} }
export function TextReact({ id, fields }: Props) { export function TextReact({ id, fields }: Props) {
...@@ -47,8 +46,8 @@ export function TextReact({ id, fields }: Props) { ...@@ -47,8 +46,8 @@ export function TextReact({ id, fields }: Props) {
const path = id.split("."); const path = id.split(".");
let entry = intl.dictionary[path.shift()!]; let entry = intl.dictionary[path.shift()!];
for (let key of path) { for (const key of path) {
// @ts-expect-error // @ts-expect-error TODO: lazy
entry = entry[key]; entry = entry[key];
} }
...@@ -57,6 +56,15 @@ export function TextReact({ id, fields }: Props) { ...@@ -57,6 +56,15 @@ export function TextReact({ id, fields }: Props) {
export function useTranslation() { export function useTranslation() {
const { intl } = useContext(IntlContext) as unknown as IntlType; const { intl } = useContext(IntlContext) as unknown as IntlType;
return (id: string, fields?: Object, plural?: number, fallback?: string) => return (
translate(id, "", intl.dictionary, fields, plural, fallback); id: string,
fields?: Record<string, string | undefined>,
plural?: number,
fallback?: string,
) => translate(id, "", intl.dictionary, fields, plural, fallback);
}
export function useDictionary() {
const { intl } = useContext(IntlContext) as unknown as IntlType;
return intl.dictionary;
} }
import { isDesktop, isMobile, isTablet } from "react-device-detect"; import { isDesktop, isMobile, isTablet } from "react-device-detect";
export const isTouchscreenDevice = export const isTouchscreenDevice =
isDesktop && !isTablet isDesktop || isTablet
? false ? false
: (typeof window !== "undefined" : (typeof window !== "undefined"
? navigator.maxTouchPoints > 0 ? navigator.maxTouchPoints > 0
......
/* eslint-disable @typescript-eslint/no-empty-function */
export const noop = () => {};
export const noopAsync = async () => {};
/* eslint-enable @typescript-eslint/no-empty-function */
/* eslint-disable react-hooks/rules-of-hooks */
import EventEmitter3 from "eventemitter3"; import EventEmitter3 from "eventemitter3";
import { Client, Message } from "revolt.js"; import { Client } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
...@@ -72,11 +74,26 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -72,11 +74,26 @@ export class SingletonRenderer extends EventEmitter3 {
this.stale = true; this.stale = true;
} }
async init(id: string) { async init(id: string, message_id?: string) {
if (message_id) {
if (this.state.type === "RENDER") {
const message = this.state.messages.find(
(x) => x._id === message_id,
);
if (message) {
this.emit("scroll", {
type: "ScrollToView",
id: message_id,
});
return;
}
}
}
this.channel = id; this.channel = id;
this.stale = false; this.stale = false;
this.setStateUnguarded({ type: "LOADING" }); this.setStateUnguarded({ type: "LOADING" });
await this.currentRenderer.init(this, id); await this.currentRenderer.init(this, id, message_id);
} }
async reloadStale(id: string) { async reloadStale(id: string) {
...@@ -93,9 +110,9 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -93,9 +110,9 @@ export class SingletonRenderer extends EventEmitter3 {
function generateScroll(end: string): ScrollState { function generateScroll(end: string): ScrollState {
if (ref) { if (ref) {
let heightRemoved = 0; let heightRemoved = 0;
let messageContainer = ref.children[0]; const messageContainer = ref.children[0];
if (messageContainer) { if (messageContainer) {
for (let child of Array.from(messageContainer.children)) { for (const child of Array.from(messageContainer.children)) {
// If this child has a ulid. // If this child has a ulid.
if (child.id?.length === 26) { if (child.id?.length === 26) {
// Check whether it was removed. // Check whether it was removed.
...@@ -107,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -107,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }
...@@ -117,12 +135,11 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -117,12 +135,11 @@ export class SingletonRenderer extends EventEmitter3 {
type: "OffsetTop", type: "OffsetTop",
previousHeight: ref.scrollHeight - heightRemoved, previousHeight: ref.scrollHeight - heightRemoved,
}; };
} else {
return {
type: "OffsetTop",
previousHeight: 0,
};
} }
return {
type: "OffsetTop",
previousHeight: 0,
};
} }
await this.currentRenderer.loadTop(this, generateScroll); await this.currentRenderer.loadTop(this, generateScroll);
...@@ -138,9 +155,9 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -138,9 +155,9 @@ export class SingletonRenderer extends EventEmitter3 {
function generateScroll(start: string): ScrollState { function generateScroll(start: string): ScrollState {
if (ref) { if (ref) {
let heightRemoved = 0; let heightRemoved = 0;
let messageContainer = ref.children[0]; const messageContainer = ref.children[0];
if (messageContainer) { if (messageContainer) {
for (let child of Array.from(messageContainer.children)) { for (const child of Array.from(messageContainer.children)) {
// If this child has a ulid. // If this child has a ulid.
if (child.id?.length === 26) { if (child.id?.length === 26) {
// Check whether it was removed. // Check whether it was removed.
...@@ -152,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -152,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }
...@@ -162,11 +180,10 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -162,11 +180,10 @@ export class SingletonRenderer extends EventEmitter3 {
type: "ScrollTop", type: "ScrollTop",
y: ref.scrollTop - heightRemoved, y: ref.scrollTop - heightRemoved,
}; };
} else {
return {
type: "ScrollToBottom",
};
} }
return {
type: "ScrollToBottom",
};
} }
await this.currentRenderer.loadBottom(this, generateScroll); await this.currentRenderer.loadBottom(this, generateScroll);
...@@ -180,7 +197,7 @@ export class SingletonRenderer extends EventEmitter3 { ...@@ -180,7 +197,7 @@ export class SingletonRenderer extends EventEmitter3 {
if (this.state.type === "RENDER" && this.state.atBottom) { if (this.state.type === "RENDER" && this.state.atBottom) {
this.emit("scroll", { type: "ScrollToBottom", smooth }); this.emit("scroll", { type: "ScrollToBottom", smooth });
} else { } else {
await this.currentRenderer.init(this, id, true); await this.currentRenderer.init(this, id, undefined, true);
} }
} }
} }
......