Make the linter happy.

This commit is contained in:
Paul 2021-08-05 14:47:00 +01:00
parent 5930415c05
commit 55e00bf93f
106 changed files with 1083 additions and 5554 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
*.log

View file

@ -1,3 +1,3 @@
{ {
"recommendations": ["esbenp.prettier-vscode"] "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
} }

View file

@ -1,112 +1,132 @@
{ {
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "rimraf build && vite build", "build": "rimraf build && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"eslintConfig": { "eslintConfig": {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": [
"preact", "preact",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"ignorePatterns": [ "ignorePatterns": [
"build/" "build/"
], ],
"rules": { "rules": {
"@typescript-eslint/explicit-module-boundary-types": "off" "radix": "off",
} "no-spaced-func": "off",
}, "react/no-danger": "off",
"dependencies": { "@typescript-eslint/explicit-module-boundary-types": "off",
"preact": "^10.5.13", "@typescript-eslint/no-non-null-assertion": "off",
"revolt-api": "0.5.1-alpha.10-patch.0" "@typescript-eslint/no-unused-vars": [
}, "warn",
"devDependencies": { {
"@fontsource/atkinson-hyperlegible": "^4.4.5", "varsIgnorePattern": "^_"
"@fontsource/bree-serif": "^4.4.5", }
"@fontsource/comic-neue": "^4.4.5", ],
"@fontsource/fira-code": "^4.4.5", "no-unused-vars": [
"@fontsource/inter": "^4.4.5", "warn",
"@fontsource/lato": "^4.4.5", {
"@fontsource/montserrat": "^4.4.5", "varsIgnorePattern": "^_"
"@fontsource/noto-sans": "^4.4.5", }
"@fontsource/open-sans": "^4.4.5", ]
"@fontsource/poppins": "^4.4.5", }
"@fontsource/raleway": "^4.4.5", },
"@fontsource/roboto": "^4.4.5", "dependencies": {
"@fontsource/roboto-mono": "^4.4.5", "vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b"
"@fontsource/source-code-pro": "^4.4.5", },
"@fontsource/space-mono": "^4.4.5", "devDependencies": {
"@fontsource/ubuntu": "^4.4.5", "@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5", "@fontsource/bree-serif": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6", "@fontsource/comic-neue": "^4.4.5",
"@preact/preset-vite": "^2.0.0", "@fontsource/fira-code": "^4.4.5",
"@rollup/plugin-replace": "^2.4.2", "@fontsource/inter": "^4.4.5",
"@styled-icons/boxicons-logos": "^10.34.0", "@fontsource/lato": "^4.4.5",
"@styled-icons/boxicons-regular": "^10.34.0", "@fontsource/montserrat": "^4.4.5",
"@styled-icons/boxicons-solid": "^10.37.0", "@fontsource/noto-sans": "^4.4.5",
"@styled-icons/simple-icons": "^10.33.0", "@fontsource/open-sans": "^4.4.5",
"@tippyjs/react": "^4.2.5", "@fontsource/poppins": "^4.4.5",
"@traptitech/markdown-it-katex": "^3.4.3", "@fontsource/raleway": "^4.4.5",
"@traptitech/markdown-it-spoiler": "^1.1.6", "@fontsource/roboto": "^4.4.5",
"@trivago/prettier-plugin-sort-imports": "^2.0.2", "@fontsource/roboto-mono": "^4.4.5",
"@types/lodash.defaultsdeep": "^4.6.6", "@fontsource/source-code-pro": "^4.4.5",
"@types/lodash.isequal": "^4.5.5", "@fontsource/space-mono": "^4.4.5",
"@types/markdown-it": "^12.0.2", "@fontsource/ubuntu": "^4.4.5",
"@types/node": "^15.12.4", "@fontsource/ubuntu-mono": "^4.4.5",
"@types/preact-i18n": "^2.3.0", "@hcaptcha/react-hcaptcha": "^0.3.6",
"@types/prismjs": "^1.16.5", "@preact/preset-vite": "^2.0.0",
"@types/react-helmet": "^6.1.1", "@rollup/plugin-replace": "^2.4.2",
"@types/react-router-dom": "^5.1.7", "@styled-icons/boxicons-logos": "^10.34.0",
"@types/react-scroll": "^1.8.2", "@styled-icons/boxicons-regular": "^10.34.0",
"@types/styled-components": "^5.1.10", "@styled-icons/boxicons-solid": "^10.37.0",
"@types/twemoji": "^12.1.1", "@styled-icons/simple-icons": "^10.33.0",
"@typescript-eslint/eslint-plugin": "^4.27.0", "@tippyjs/react": "^4.2.5",
"@typescript-eslint/parser": "^4.27.0", "@traptitech/markdown-it-katex": "^3.4.3",
"classnames": "^2.3.1", "@traptitech/markdown-it-spoiler": "^1.1.6",
"dayjs": "^1.10.6", "@trivago/prettier-plugin-sort-imports": "^2.0.2",
"detect-browser": "^5.2.0", "@types/lodash.defaultsdeep": "^4.6.6",
"eslint": "^7.28.0", "@types/lodash.isequal": "^4.5.5",
"eslint-config-preact": "^1.1.4", "@types/markdown-it": "^12.0.2",
"eventemitter3": "^4.0.7", "@types/node": "^15.12.4",
"highlight.js": "^11.0.1", "@types/preact-i18n": "^2.3.0",
"idb": "^6.1.2", "@types/prismjs": "^1.16.5",
"localforage": "^1.9.0", "@types/react-helmet": "^6.1.1",
"lodash.defaultsdeep": "^4.6.1", "@types/react-router-dom": "^5.1.7",
"lodash.isequal": "^4.5.0", "@types/react-scroll": "^1.8.2",
"markdown-it": "^12.0.6", "@types/styled-components": "^5.1.10",
"markdown-it-emoji": "^2.0.0", "@types/twemoji": "^12.1.1",
"markdown-it-sub": "^1.0.0", "@typescript-eslint/eslint-plugin": "^4.27.0",
"markdown-it-sup": "^1.0.0", "@typescript-eslint/parser": "^4.27.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext", "classnames": "^2.3.1",
"mobx": "^6.3.2", "dayjs": "^1.10.6",
"mobx-react-lite": "^3.2.0", "detect-browser": "^5.2.0",
"preact-context-menu": "^0.1.5", "eslint": "^7.28.0",
"preact-i18n": "^2.4.0-preactx", "eslint-config-preact": "^1.1.4",
"prettier": "^2.3.1", "eventemitter3": "^4.0.7",
"prismjs": "^1.23.0", "highlight.js": "^11.0.1",
"react-device-detect": "^1.17.0", "localforage": "^1.9.0",
"react-helmet": "^6.1.0", "lodash.defaultsdeep": "^4.6.1",
"react-hook-form": "6.3.0", "lodash.isequal": "^4.5.0",
"react-overlapping-panels": "1.2.2", "markdown-it": "^12.0.6",
"react-redux": "^7.2.4", "markdown-it-emoji": "^2.0.0",
"react-router-dom": "^5.2.0", "markdown-it-sub": "^1.0.0",
"react-scroll": "^1.8.2", "markdown-it-sup": "^1.0.0",
"redux": "^4.1.0", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"revolt.js": "5.0.0-alpha.18", "mobx": "^6.3.2",
"rimraf": "^3.0.2", "mobx-react-lite": "^3.2.0",
"sass": "^1.35.1", "preact": "^10.5.14",
"shade-blend-color": "^1.0.0", "preact-context-menu": "^0.1.5",
"styled-components": "^5.3.0", "preact-i18n": "^2.4.0-preactx",
"typescript": "^4.3.2", "prettier": "^2.3.1",
"ulid": "^2.3.0", "prismjs": "^1.23.0",
"use-resize-observer": "^7.0.0", "react-device-detect": "^1.17.0",
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b", "react-helmet": "^6.1.0",
"vite-plugin-pwa": "^0.8.1", "react-hook-form": "6.3.0",
"workbox-precaching": "^6.1.5" "react-overlapping-panels": "1.2.2",
} "react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2",
"redux": "^4.1.0",
"revolt-api": "0.5.1-alpha.10-patch.0",
"revolt.js": "5.0.0-alpha.18",
"rimraf": "^3.0.2",
"sass": "^1.35.1",
"shade-blend-color": "^1.0.0",
"styled-components": "^5.3.0",
"typescript": "^4.3.2",
"ulid": "^2.3.0",
"use-resize-observer": "^7.0.0",
"vite-plugin-pwa": "^0.8.1",
"workbox-precaching": "^6.1.5"
},
"name": "client",
"main": "index.js",
"repository": "https://gitlab.insrt.uk/revolt/revite.git",
"author": "Paul <paulmakles@gmail.com>",
"license": "MIT"
} }

View file

@ -1,4 +1,3 @@
import { useStore } from "react-redux";
import { SYSTEM_USER_ID } from "revolt.js"; import { SYSTEM_USER_ID } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js/dist/maps/Users";
@ -143,14 +142,16 @@ export function useAutoComplete(
) as User[]; ) as User[];
break; break;
case "TextChannel": case "TextChannel":
const server = channel.server_id; {
users = [...client.members.keys()] const server = channel.server_id;
.map((x) => JSON.parse(x)) users = [...client.members.keys()]
.filter((x) => x.server === server) .map((x) => JSON.parse(x))
.map((x) => client.users.get(x.user)) .filter((x) => x.server === server)
.filter( .map((x) => client.users.get(x.user))
(x) => typeof x !== "undefined", .filter(
) as User[]; (x) => typeof x !== "undefined",
) as User[];
}
break; break;
default: default:
return; return;
@ -304,7 +305,7 @@ export function useAutoComplete(
function onKeyUp(e: KeyboardEvent) { function onKeyUp(e: KeyboardEvent) {
if (e.currentTarget !== null) { if (e.currentTarget !== null) {
// @ts-expect-error // @ts-expect-error Type mis-match.
onChange(e); onChange(e);
} }
} }
@ -391,6 +392,7 @@ export default function AutoComplete({
{state.type === "emoji" && {state.type === "emoji" &&
state.matches.map((match, i) => ( state.matches.map((match, i) => (
<button <button
key={match}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&
@ -422,6 +424,7 @@ export default function AutoComplete({
{state.type === "user" && {state.type === "user" &&
state.matches.map((match, i) => ( state.matches.map((match, i) => (
<button <button
key={match}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&
@ -446,6 +449,7 @@ export default function AutoComplete({
{state.type === "channel" && {state.type === "channel" &&
state.matches.map((match, i) => ( state.matches.map((match, i) => (
<button <button
key={match}
className={i === state.selected ? "active" : ""} className={i === state.selected ? "active" : ""}
onMouseEnter={() => onMouseEnter={() =>
(i !== state.selected || !state.within) && (i !== state.selected || !state.within) &&

View file

@ -15,7 +15,11 @@ interface Props extends IconBaseProps<Channel> {
export default observer( export default observer(
( (
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>, props: Props &
Omit<
JSX.HTMLAttributes<HTMLImageElement>,
keyof Props | "children" | "as"
>,
) => { ) => {
const client = useContext(AppContext); const client = useContext(AppContext);
@ -25,8 +29,6 @@ export default observer(
attachment, attachment,
isServerChannel: server, isServerChannel: server,
animate, animate,
children,
as,
...imgProps ...imgProps
} = props; } = props;
const iconURL = client.generateFileURL( const iconURL = client.generateFileURL(

View file

@ -22,7 +22,7 @@ export function LocaleSelector(props: Props) {
{Object.keys(Languages).map((x) => { {Object.keys(Languages).map((x) => {
const l = Languages[x as keyof typeof Languages]; const l = Languages[x as keyof typeof Languages];
return ( return (
<option value={x}> <option value={x} key={x}>
{l.emoji} {l.display} {l.emoji} {l.display}
</option> </option>
); );

View file

@ -22,23 +22,19 @@ const ServerText = styled.div`
background: var(--primary-background); background: var(--primary-background);
`; `;
const fallback = "/assets/group.png"; // const fallback = "/assets/group.png";
export default observer( export default observer(
( (
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>, props: Props &
Omit<
JSX.HTMLAttributes<HTMLImageElement>,
keyof Props | "children" | "as"
>,
) => { ) => {
const client = useContext(AppContext); const client = useContext(AppContext);
const { const { target, attachment, size, animate, server_name, ...imgProps } =
target, props;
attachment,
size,
animate,
server_name,
children,
as,
...imgProps
} = props;
const iconURL = client.generateFileURL( const iconURL = client.generateFileURL(
target?.icon ?? attachment, target?.icon ?? attachment,
{ max_side: 256 }, { max_side: 256 },

View file

@ -16,7 +16,7 @@ export default function Tooltip(props: Props) {
return ( return (
<Tippy content={content} {...tippyProps}> <Tippy content={content} {...tippyProps}>
{/* {/*
// @ts-expect-error */} // @ts-expect-error Type mis-match. */}
<div style={`display: flex;`}>{children}</div> <div style={`display: flex;`}>{children}</div>
</Tippy> </Tippy>
); );

View file

@ -1,5 +1,5 @@
import { Download } from "@styled-icons/boxicons-regular"; /* eslint-disable react-hooks/rules-of-hooks */
import { CloudDownload } from "@styled-icons/boxicons-regular"; import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";

View file

@ -60,6 +60,7 @@ const Message = observer(
? (attachContextMenu("Menu", { ? (attachContextMenu("Menu", {
user: message.author_id, user: message.author_id,
contextualChannel: message.channel_id, contextualChannel: message.channel_id,
// eslint-disable-next-line
}) as any) }) as any)
: undefined; : undefined;
@ -73,6 +74,7 @@ const Message = observer(
<div id={message._id}> <div id={message._id}>
{message.reply_ids?.map((message_id, index) => ( {message.reply_ids?.map((message_id, index) => (
<MessageReply <MessageReply
key={message_id}
index={index} index={index}
id={message_id} id={message_id}
channel={message.channel!} channel={message.channel!}

View file

@ -6,7 +6,6 @@ import { decodeTime } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useDictionary } from "../../../lib/i18n"; import { useDictionary } from "../../../lib/i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { dayjs } from "../../../context/Locale"; import { dayjs } from "../../../context/Locale";

View file

@ -141,22 +141,25 @@ export default observer(({ channel }: Props) => {
); );
} }
function setMessage(content?: string) { const setMessage = useCallback(
setDraft(content ?? ""); (content?: string) => {
setDraft(content ?? "");
if (content) { if (content) {
dispatch({ dispatch({
type: "SET_DRAFT", type: "SET_DRAFT",
channel: channel._id, channel: channel._id,
content, content,
}); });
} else { } else {
dispatch({ dispatch({
type: "CLEAR_DRAFT", type: "CLEAR_DRAFT",
channel: channel._id, channel: channel._id,
}); });
} }
} },
[channel._id],
);
useEffect(() => { useEffect(() => {
function append(content: string, action: "quote" | "mention") { function append(content: string, action: "quote" | "mention") {
@ -175,8 +178,12 @@ export default observer(({ channel }: Props) => {
} }
} }
return internalSubscribe("MessageBox", "append", append); return internalSubscribe(
}, [draft]); "MessageBox",
"append",
append as (...args: unknown[]) => void,
);
}, [draft, setMessage]);
async function send() { async function send() {
if (uploadState.type === "uploading" || uploadState.type === "sending") if (uploadState.type === "uploading" || uploadState.type === "sending")
@ -344,9 +351,11 @@ export default observer(({ channel }: Props) => {
} }
} }
const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [ // eslint-disable-next-line
channel._id, const debouncedStopTyping = useCallback(
]); debounce(stopTyping as (...args: unknown[]) => void, 1000),
[channel._id],
);
const { const {
onChange, onChange,
onKeyUp, onKeyUp,
@ -478,7 +487,7 @@ export default observer(({ channel }: Props) => {
: channel.channel_type === "SavedMessages" : channel.channel_type === "SavedMessages"
? translate("app.main.channel.message_saved") ? translate("app.main.channel.message_saved")
: translate("app.main.channel.message_where", { : translate("app.main.channel.message_where", {
channel_name: channel.name, channel_name: channel.name ?? undefined,
}) })
} }
disabled={ disabled={

View file

@ -1,4 +1,3 @@
import { Reply } from "@styled-icons/boxicons-regular";
import { File } from "@styled-icons/boxicons-solid"; import { File } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@ -13,8 +12,6 @@ import { useLayoutEffect, useState } from "preact/hooks";
import { useRenderState } from "../../../../lib/renderer/Singleton"; import { useRenderState } from "../../../../lib/renderer/Singleton";
import { useClient } from "../../../../context/revoltjs/RevoltClient";
import Markdown from "../../../markdown/Markdown"; import Markdown from "../../../markdown/Markdown";
import UserShort from "../../user/UserShort"; import UserShort from "../../user/UserShort";
import { SystemMessage } from "../SystemMessage"; import { SystemMessage } from "../SystemMessage";
@ -136,10 +133,6 @@ export const ReplyBase = styled.div<{
`} `}
`; `;
const Arrow = styled.div`
`;
export const MessageReply = observer(({ index, channel, id }: Props) => { export const MessageReply = observer(({ index, channel, id }: Props) => {
const view = useRenderState(channel._id); const view = useRenderState(channel._id);
if (view?.type !== "RENDER") return null; if (view?.type !== "RENDER") return null;
@ -155,7 +148,7 @@ export const MessageReply = observer(({ index, channel, id }: Props) => {
} else { } else {
channel.fetchMessage(id).then(setMessage); channel.fetchMessage(id).then(setMessage);
} }
}, [view.messages]); }, [id, channel, view.messages]);
if (!message) { if (!message) {
return ( return (
@ -204,9 +197,12 @@ export const MessageReply = observer(({ index, channel, id }: Props) => {
{message.attachments && ( {message.attachments && (
<> <>
<File size={16} /> <File size={16} />
<em>{message.attachments.length > 1 ? <em>
<Text id="app.main.channel.misc.sent_multiple_files" /> : {message.attachments.length > 1 ? (
<Text id="app.main.channel.misc.sent_file" /> } <Text id="app.main.channel.misc.sent_multiple_files" />
) : (
<Text id="app.main.channel.misc.sent_file" />
)}
</em> </em>
</> </>
)} )}

View file

@ -60,7 +60,7 @@ export default function TextFile({ attachment }: Props) {
setLoading(false); setLoading(false);
}); });
} }
}, [content, loading, status]); }, [content, loading, status, attachment._id, attachment.size, url]);
return ( return (
<div <div

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular"; import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
import styled from "styled-components"; import styled from "styled-components";
@ -186,7 +187,9 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
<Container> <Container>
<Carousel> <Carousel>
{state.files.map((file, index) => ( {state.files.map((file, index) => (
<> // @ts-expect-error brokey
// eslint-disable-next-line react/jsx-no-undef
<Fragment key={file.name}>
{index === CAN_UPLOAD_AT_ONCE && <Divider />} {index === CAN_UPLOAD_AT_ONCE && <Divider />}
<FileEntry <FileEntry
index={index} index={index}
@ -198,7 +201,7 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
: undefined : undefined
} }
/> />
</> </Fragment>
))} ))}
{state.type === "attached" && ( {state.type === "attached" && (
<EmptyEntry onClick={addFile}> <EmptyEntry onClick={addFile}>

View file

@ -83,9 +83,9 @@ export default observer(({ channel, replies, setReplies }: Props) => {
(id) => (id) =>
replies.length < MAX_REPLIES && replies.length < MAX_REPLIES &&
!replies.find((x) => x.id === id) && !replies.find((x) => x.id === id) &&
setReplies([...replies, { id, mention: false }]), setReplies([...replies, { id: id as string, mention: false }]),
); );
}, [replies]); }, [replies, setReplies]);
const view = useRenderState(channel); const view = useRenderState(channel);
if (view?.type !== "RENDER") return null; if (view?.type !== "RENDER") return null;
@ -116,25 +116,28 @@ export default observer(({ channel, replies, setReplies }: Props) => {
<UserShort user={message.author} size={16} /> <UserShort user={message.author} size={16} />
</div> </div>
<div class="message"> <div class="message">
{message.attachments && ( {message.attachments && (
<> <>
<File size={16} /> <File size={16} />
<em>{message.attachments.length > 1 ? <em>
<Text id="app.main.channel.misc.sent_multiple_files" /> : {message.attachments.length > 1 ? (
<Text id="app.main.channel.misc.sent_file" /> } <Text id="app.main.channel.misc.sent_multiple_files" />
) : (
<Text id="app.main.channel.misc.sent_file" />
)}
</em> </em>
</> </>
)} )}
{message.author_id === SYSTEM_USER_ID ? ( {message.author_id === SYSTEM_USER_ID ? (
<SystemMessage message={message} /> <SystemMessage message={message} />
) : ( ) : (
<Markdown <Markdown
disallowBigEmoji disallowBigEmoji
content={( content={(
message.content as string message.content as string
).replace(/\n/g, " ")} ).replace(/\n/g, " ")}
/> />
)} )}
</div> </div>
</ReplyBase> </ReplyBase>
<span class="actions"> <span class="actions">

View file

@ -5,10 +5,6 @@ import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { connectState } from "../../../../redux/connector";
import { useClient } from "../../../../context/revoltjs/RevoltClient";
interface Props { interface Props {
channel: Channel; channel: Channel;
} }
@ -104,6 +100,7 @@ export default observer(({ channel }: Props) => {
<div className="avatars"> <div className="avatars">
{users.map((user) => ( {users.map((user) => (
<img <img
key={user!._id}
loading="eager" loading="eager"
src={user!.generateAvatarURL({ max_side: 256 })} src={user!.generateAvatarURL({ max_side: 256 })}
/> />

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Embed } from "revolt-api/types/January"; import { Embed } from "revolt-api/types/January";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";

View file

@ -5,8 +5,7 @@ import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components"; import styled from "styled-components";
import { openContextMenu } from "preact-context-menu"; import { openContextMenu } from "preact-context-menu";
import { Text } from "preact-i18n"; import { Text, Localizer } from "preact-i18n";
import { Localizer } from "preact-i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";

View file

@ -52,20 +52,23 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
`; `;
export default observer( export default observer(
(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) => { (
props: Props &
Omit<
JSX.SVGAttributes<SVGSVGElement>,
keyof Props | "children" | "as"
>,
) => {
const client = useContext(AppContext); const client = useContext(AppContext);
const { const {
target, target,
attachment, attachment,
size, size,
voice,
status, status,
animate, animate,
mask, mask,
hover, hover,
children,
as,
...svgProps ...svgProps
} = props; } = props;
const iconURL = const iconURL =

View file

@ -31,10 +31,10 @@ export const Username = observer(
} }
if (member.roles && member.roles.length > 0) { if (member.roles && member.roles.length > 0) {
let srv = client.servers.get(member._id.server); const srv = client.servers.get(member._id.server);
if (srv?.roles) { if (srv?.roles) {
for (let role of member.roles) { for (const role of member.roles) {
let c = srv.roles[role].colour; const c = srv.roles[role].colour;
if (c) { if (c) {
color = c; color = c;
continue; continue;

View file

@ -9,7 +9,7 @@ export interface MarkdownProps {
export default function Markdown(props: MarkdownProps) { export default function Markdown(props: MarkdownProps) {
return ( return (
// @ts-expect-error // @ts-expect-error Typings mis-match.
<Suspense fallback={props.content}> <Suspense fallback={props.content}>
<Renderer {...props} /> <Renderer {...props} />
</Suspense> </Suspense>

View file

@ -1,19 +1,20 @@
/* eslint-disable react-hooks/rules-of-hooks */
import MarkdownKatex from "@traptitech/markdown-it-katex"; import MarkdownKatex from "@traptitech/markdown-it-katex";
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler"; import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
// @ts-ignore // @ts-expect-error No typings.
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare"; import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
// @ts-ignore // @ts-expect-error No typings.
import MarkdownSub from "markdown-it-sub"; import MarkdownSub from "markdown-it-sub";
// @ts-ignore // @ts-expect-error No typings.
import MarkdownSup from "markdown-it-sup"; import MarkdownSup from "markdown-it-sup";
import Prism from "prismjs"; import Prism from "prismjs";
import "prismjs/themes/prism-tomorrow.css"; import "prismjs/themes/prism-tomorrow.css";
import { RE_MENTIONS } from "revolt.js"; import { RE_MENTIONS } from "revolt.js";
import styles from "./Markdown.module.scss"; import styles from "./Markdown.module.scss";
import { useCallback, useContext, useRef } from "preact/hooks"; import { useCallback, useContext } from "preact/hooks";
import { internalEmit } from "../../lib/eventEmitter"; import { internalEmit } from "../../lib/eventEmitter";
@ -95,8 +96,8 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
// We replace the message with the mention at the time of render. // We replace the message with the mention at the time of render.
// We don't care if the mention changes. // We don't care if the mention changes.
const newContent = content const newContent = content
.replace(RE_MENTIONS, (sub: string, ...args: any[]) => { .replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
const id = args[0], const id = args[0] as string,
user = client.users.get(id); user = client.users.get(id);
if (user) { if (user) {
@ -105,8 +106,8 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
return sub; return sub;
}) })
.replace(RE_CHANNELS, (sub: string, ...args: any[]) => { .replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
const id = args[0], const id = args[0] as string,
channel = client.channels.get(id); channel = client.channels.get(id);
if (channel?.channel_type === "TextChannel") { if (channel?.channel_type === "TextChannel") {

View file

@ -1,7 +1,6 @@
import { Wrench, Microphone, VolumeFull } from "@styled-icons/boxicons-solid"; import { Wrench } from "@styled-icons/boxicons-solid";
import styled from "styled-components"; import styled from "styled-components";
import Tooltip from "../common/Tooltip";
import UpdateIndicator from "../common/UpdateIndicator"; import UpdateIndicator from "../common/UpdateIndicator";
const TitlebarBase = styled.div` const TitlebarBase = styled.div`
@ -20,7 +19,8 @@ const TitlebarBase = styled.div`
.quick { .quick {
color: var(--secondary-foreground); color: var(--secondary-foreground);
> div, > div > div { > div,
> div > div {
width: var(--titlebar-height) !important; width: var(--titlebar-height) !important;
} }
@ -99,7 +99,9 @@ export function Titlebar() {
stroke-width="1" stroke-width="1"
/> />
</svg> </svg>
{window.native.getConfig().build === "dev" && <Wrench size="12.5"/>} {window.native.getConfig().build === "dev" && (
<Wrench size="12.5" />
)}
</div> </div>
{/*<div class="actions quick"> {/*<div class="actions quick">
<Tooltip <Tooltip
@ -121,13 +123,50 @@ export function Titlebar() {
<UpdateIndicator style="titlebar" /> <UpdateIndicator style="titlebar" />
<div class="actions"> <div class="actions">
<div onClick={window.native.min}> <div onClick={window.native.min}>
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><rect fill="currentColor" width="10" height="1" x="1" y="6"></rect></svg> <svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<rect
fill="currentColor"
width="10"
height="1"
x="1"
y="6"
/>
</svg>
</div> </div>
<div onClick={window.native.max}> <div onClick={window.native.max}>
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="currentColor"></rect></svg> <svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<rect
width="9"
height="9"
x="1.5"
y="1.5"
fill="none"
stroke="currentColor"
/>
</svg>
</div> </div>
<div onClick={window.native.close} class="error"> <div onClick={window.native.close} class="error">
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><polygon fill="currentColor" stroke-width="1" fill-rule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1" style="stroke:currentColor;stroke-width:0.4"></polygon></svg> <svg
aria-hidden="false"
width="12"
height="12"
viewBox="0 0 12 12">
<polygon
fill="currentColor"
stroke-width="1"
fill-rule="evenodd"
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"
style="stroke:currentColor;stroke-width:0.4"
/>
</svg>
</div> </div>
</div> </div>
</TitlebarBase> </TitlebarBase>

View file

@ -148,6 +148,7 @@ const HomeSidebar = observer((props: Props) => {
return ( return (
<ConditionalLink <ConditionalLink
key={x.channel._id}
active={x.channel._id === channel} active={x.channel._id === channel}
to={`/channel/${x.channel._id}`}> to={`/channel/${x.channel._id}`}>
<ChannelButton <ChannelButton

View file

@ -279,6 +279,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
return ( return (
<ConditionalLink <ConditionalLink
key={entry.server._id}
active={active} active={active}
to={`/server/${entry.server._id}${ to={`/server/${entry.server._id}${
id ? `/channel/${id}` : "" id ? `/channel/${id}` : ""

View file

@ -7,11 +7,11 @@ import { useEffect } from "preact/hooks";
import ConditionalLink from "../../../lib/ConditionalLink"; import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter"; import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
@ -38,9 +38,9 @@ const ServerBase = styled.div`
overflow: hidden; overflow: hidden;
${isTouchscreenDevice && ${isTouchscreenDevice &&
css` css`
padding-bottom: 50px; padding-bottom: 50px;
`} `}
`; `;
const ServerList = styled.div` const ServerList = styled.div`
@ -73,7 +73,7 @@ const ServerSidebar = observer((props: Props) => {
parent: server_id!, parent: server_id!,
child: channel_id!, child: channel_id!,
}); });
}, [channel_id]); }, [channel_id, server_id]);
const uncategorised = new Set(server.channel_ids); const uncategorised = new Set(server.channel_ids);
const elements = []; const elements = [];

View file

@ -1,4 +1,4 @@
import { autorun, isObservableProp, reaction } from "mobx"; import { reaction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { useLayoutEffect } from "preact/hooks"; import { useLayoutEffect } from "preact/hooks";
@ -6,16 +6,12 @@ import { useLayoutEffect } from "preact/hooks";
import { dispatch } from "../../../redux"; import { dispatch } from "../../../redux";
import { Unreads } from "../../../redux/reducers/unreads"; import { Unreads } from "../../../redux/reducers/unreads";
import { useClient } from "../../../context/revoltjs/RevoltClient";
type UnreadProps = { type UnreadProps = {
channel: Channel; channel: Channel;
unreads: Unreads; unreads: Unreads;
}; };
export function useUnreads({ channel, unreads }: UnreadProps) { export function useUnreads({ channel, unreads }: UnreadProps) {
const client = useClient();
useLayoutEffect(() => { useLayoutEffect(() => {
function checkUnread(target: Channel) { function checkUnread(target: Channel) {
if (!target) return; if (!target) return;

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useRenderState } from "../../../lib/renderer/Singleton"; import { useRenderState } from "../../../lib/renderer/Singleton";
interface Props { interface Props {

View file

@ -1,11 +1,9 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { Presence } from "revolt-api/types/Users"; import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import Members, { Member } from "revolt.js/dist/maps/Members";
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js/dist/maps/Users";
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
@ -14,7 +12,6 @@ import { getState } from "../../../redux";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { import {
AppContext,
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient, useClient,
@ -50,7 +47,6 @@ export const GroupMemberSidebar = observer(
({ channel }: { channel: Channel }) => { ({ channel }: { channel: Channel }) => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useClient();
const members = channel.recipients?.filter( const members = channel.recipients?.filter(
(x) => typeof x !== "undefined", (x) => typeof x !== "undefined",
); );
@ -173,9 +169,9 @@ export const ServerMemberSidebar = observer(
if (status === ClientStatus.ONLINE) { if (status === ClientStatus.ONLINE) {
channel.server!.fetchMembers(); channel.server!.fetchMembers();
} }
}, [status]); }, [status, channel.server]);
let users = [...client.members.keys()] const users = [...client.members.keys()]
.map((x) => JSON.parse(x)) .map((x) => JSON.parse(x))
.filter((x) => x.server === channel.server_id) .filter((x) => x.server === channel.server_id)
.map((y) => client.users.get(y.user)!) .map((y) => client.users.get(y.user)!)
@ -247,7 +243,6 @@ export const ServerMemberSidebar = observer(
function Search({ channel }: { channel: Channel }) { function Search({ channel }: { channel: Channel }) {
if (!getState().experiments.enabled?.includes("search")) return null; if (!getState().experiments.enabled?.includes("search")) return null;
const client = useContext(AppContext);
type Sort = "Relevance" | "Latest" | "Oldest"; type Sort = "Relevance" | "Latest" | "Oldest";
const [sort, setSort] = useState<Sort>("Relevance"); const [sort, setSort] = useState<Sort>("Relevance");
@ -272,6 +267,7 @@ function Search({ channel }: { channel: Channel }) {
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
{["Relevance", "Latest", "Oldest"].map((key) => ( {["Relevance", "Latest", "Oldest"].map((key) => (
<Button <Button
key={key}
style={{ flex: 1, minWidth: 0 }} style={{ flex: 1, minWidth: 0 }}
compact compact
error={sort === key} error={sort === key}
@ -304,7 +300,7 @@ function Search({ channel }: { channel: Channel }) {
href += `/channel/${message.channel_id}/${message._id}`; href += `/channel/${message.channel_id}/${message._id}`;
return ( return (
<Link to={href}> <Link to={href} key={message._id}>
<div <div
style={{ style={{
margin: "2px", margin: "2px",

View file

@ -1,6 +1,7 @@
/* eslint-disable react-hooks/rules-of-hooks */
import styled, { css, keyframes } from "styled-components"; import styled, { css, keyframes } from "styled-components";
import { createPortal, useEffect, useState } from "preact/compat"; import { createPortal, useCallback, useEffect, useState } from "preact/compat";
import { internalSubscribe } from "../../lib/eventEmitter"; import { internalSubscribe } from "../../lib/eventEmitter";
@ -134,7 +135,7 @@ interface Props {
dontModal?: boolean; dontModal?: boolean;
padding?: boolean; padding?: boolean;
onClose: () => void; onClose?: () => void;
actions?: Action[]; actions?: Action[];
disabled?: boolean; disabled?: boolean;
border?: boolean; border?: boolean;
@ -163,12 +164,12 @@ export default function Modal(props: Props) {
const [animateClose, setAnimateClose] = useState(false); const [animateClose, setAnimateClose] = useState(false);
isModalClosing = animateClose; isModalClosing = animateClose;
function onClose() { const onClose = useCallback(() => {
setAnimateClose(true); setAnimateClose(true);
setTimeout(() => props.onClose(), 2e2); setTimeout(() => props.onClose?.(), 2e2);
} }, [setAnimateClose, props]);
useEffect(() => internalSubscribe("Modal", "close", onClose), []); useEffect(() => internalSubscribe("Modal", "close", onClose), [onClose]);
useEffect(() => { useEffect(() => {
if (props.disallowClosing) return; if (props.disallowClosing) return;
@ -181,7 +182,7 @@ export default function Modal(props: Props) {
document.body.addEventListener("keydown", keyDown); document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown); return () => document.body.removeEventListener("keydown", keyDown);
}, [props.disallowClosing, props.onClose]); }, [props.disallowClosing, onClose]);
const confirmationAction = props.actions?.find( const confirmationAction = props.actions?.find(
(action) => action.confirmation, (action) => action.confirmation,
@ -211,8 +212,12 @@ export default function Modal(props: Props) {
{content} {content}
{props.actions && ( {props.actions && (
<ModalActions> <ModalActions>
{props.actions.map((x) => ( {props.actions.map((x, index) => (
<Button {...x} disabled={props.disabled} /> <Button
key={index}
{...x}
disabled={props.disabled}
/>
))} ))}
</ModalActions> </ModalActions>
)} )}

View file

@ -64,7 +64,7 @@ export default function Tip(
{!hideSeparator && <Separator />} {!hideSeparator && <Separator />}
<TipBase {...tipProps}> <TipBase {...tipProps}>
<InfoCircle size={20} /> <InfoCircle size={20} />
<span>{props.children}</span> <span>{children}</span>
</TipBase> </TipBase>
</> </>
); );

View file

@ -4,6 +4,7 @@ import styled, { css } from "styled-components";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
interface BaseProps { interface BaseProps {
readonly hover?: boolean;
readonly account?: boolean; readonly account?: boolean;
readonly disabled?: boolean; readonly disabled?: boolean;
readonly largeDescription?: boolean; readonly largeDescription?: boolean;
@ -25,8 +26,6 @@ const CategoryBase = styled.div<BaseProps>`
flex-shrink: 0; flex-shrink: 0;
} }
.content { .content {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
@ -41,8 +40,6 @@ const CategoryBase = styled.div<BaseProps>`
overflow: hidden; overflow: hidden;
} }
.description { .description {
${(props) => ${(props) =>
props.largeDescription props.largeDescription
@ -66,7 +63,7 @@ const CategoryBase = styled.div<BaseProps>`
} }
${(props) => ${(props) =>
typeof props.onClick !== "undefined" && props.hover &&
css` css`
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
@ -80,7 +77,7 @@ const CategoryBase = styled.div<BaseProps>`
${(props) => ${(props) =>
props.disabled && props.disabled &&
css` css`
opacity: .4; opacity: 0.4;
/*.content, /*.content,
.action { .action {
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
@ -133,10 +130,12 @@ export default function CategoryButton({
account, account,
disabled, disabled,
onClick, onClick,
hover,
action, action,
}: Props) { }: Props) {
return ( return (
<CategoryBase <CategoryBase
hover={hover || typeof onClick !== "undefined"}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
account={account}> account={account}>

View file

@ -5,7 +5,7 @@ import update from "dayjs/plugin/updateLocale";
import defaultsDeep from "lodash.defaultsdeep"; import defaultsDeep from "lodash.defaultsdeep";
import { IntlProvider } from "preact-i18n"; import { IntlProvider } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useCallback, useEffect, useState } from "preact/hooks";
import { connectState } from "../redux/connector"; import { connectState } from "../redux/connector";
@ -165,12 +165,14 @@ export interface Dictionary {
} }
function Locale({ children, locale }: Props) { function Locale({ children, locale }: Props) {
const [defns, setDefinition] = useState<Dictionary>(definition as any); const [defns, setDefinition] = useState<Dictionary>(
definition as Dictionary,
);
// Load relevant language information, fallback to English if invalid. // Load relevant language information, fallback to English if invalid.
const lang = Languages[locale] ?? Languages.en; const lang = Languages[locale] ?? Languages.en;
function transformLanguage(source: { [key: string]: any }) { function transformLanguage(source: Dictionary) {
// Fallback untranslated strings to English (UK) // Fallback untranslated strings to English (UK)
const obj = defaultsDeep(source, definition); const obj = defaultsDeep(source, definition);
@ -216,43 +218,46 @@ function Locale({ children, locale }: Props) {
return obj; return obj;
} }
function loadLanguage(locale: string) { const loadLanguage = useCallback(
if (locale === "en") { (locale: string) => {
// If English, make sure to restore everything to defaults. if (locale === "en") {
// Use what we already have. // If English, make sure to restore everything to defaults.
const defn = transformLanguage(definition); // Use what we already have.
setDefinition(defn); const defn = transformLanguage(definition as Dictionary);
dayjs.locale("en");
dayjs.updateLocale("en", { calendar: defn.dayjs });
return;
}
import(`../../external/lang/${lang.i18n}.json`).then(
async (lang_file) => {
// Transform the definitions data.
const defn = transformLanguage(lang_file.default);
// Determine and load dayjs locales.
const target = lang.dayjs ?? lang.i18n;
const dayjs_locale = await import(
`../../node_modules/dayjs/esm/locale/${target}.js`
);
// Load dayjs locales.
dayjs.locale(target, dayjs_locale.default);
if (defn.dayjs) {
// Override dayjs calendar locales with our own.
dayjs.updateLocale(target, { calendar: defn.dayjs });
}
// Apply definition to app.
setDefinition(defn); setDefinition(defn);
}, dayjs.locale("en");
); dayjs.updateLocale("en", { calendar: defn.dayjs });
} return;
}
useEffect(() => loadLanguage(locale), [locale, lang]); import(`../../external/lang/${lang.i18n}.json`).then(
async (lang_file) => {
// Transform the definitions data.
const defn = transformLanguage(lang_file.default);
// Determine and load dayjs locales.
const target = lang.dayjs ?? lang.i18n;
const dayjs_locale = await import(
`../../node_modules/dayjs/esm/locale/${target}.js`
);
// Load dayjs locales.
dayjs.locale(target, dayjs_locale.default);
if (defn.dayjs) {
// Override dayjs calendar locales with our own.
dayjs.updateLocale(target, { calendar: defn.dayjs });
}
// Apply definition to app.
setDefinition(defn);
},
);
},
[lang.dayjs, lang.i18n],
);
useEffect(() => loadLanguage(locale), [locale, lang, loadLanguage]);
useEffect(() => { useEffect(() => {
// Apply RTL language format. // Apply RTL language format.

View file

@ -4,8 +4,6 @@ import { createGlobalStyle } from "styled-components";
import { createContext } from "preact"; import { createContext } from "preact";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
import { connectState } from "../redux/connector"; import { connectState } from "../redux/connector";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
@ -311,17 +309,17 @@ function Theme({ children, options }: Props) {
const font = theme.font ?? DEFAULT_FONT; const font = theme.font ?? DEFAULT_FONT;
root.setProperty("--font", `"${font}"`); root.setProperty("--font", `"${font}"`);
FONTS[font].load(); FONTS[font].load();
}, [theme.font]); }, [root, theme.font]);
useEffect(() => { useEffect(() => {
const font = theme.monospaceFont ?? DEFAULT_MONO_FONT; const font = theme.monospaceFont ?? DEFAULT_MONO_FONT;
root.setProperty("--monospace-font", `"${font}"`); root.setProperty("--monospace-font", `"${font}"`);
MONOSPACE_FONTS[font].load(); MONOSPACE_FONTS[font].load();
}, [theme.monospaceFont]); }, [root, theme.monospaceFont]);
useEffect(() => { useEffect(() => {
root.setProperty("--ligatures", options?.ligatures ? "normal" : "none"); root.setProperty("--ligatures", options?.ligatures ? "normal" : "none");
}, [options?.ligatures]); }, [root, options?.ligatures]);
useEffect(() => { useEffect(() => {
const resize = () => const resize = () =>
@ -330,7 +328,7 @@ function Theme({ children, options }: Props) {
window.addEventListener("resize", resize); window.addEventListener("resize", resize);
return () => window.removeEventListener("resize", resize); return () => window.removeEventListener("resize", resize);
}, []); }, [root]);
return ( return (
<ThemeContext.Provider value={theme}> <ThemeContext.Provider value={theme}>

View file

@ -1,14 +1,20 @@
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import { createContext } from "preact"; import { createContext } from "preact";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "preact/hooks";
import type { ProduceType, VoiceUser } from "../lib/vortex/Types"; import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
import type VoiceClient from "../lib/vortex/VoiceClient"; import type VoiceClient from "../lib/vortex/VoiceClient";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
import { SoundContext } from "./Settings"; import { SoundContext } from "./Settings";
import { AppContext } from "./revoltjs/RevoltClient";
export enum VoiceStatus { export enum VoiceStatus {
LOADING = 0, LOADING = 0,
@ -45,20 +51,22 @@ type Props = {
}; };
export default function Voice({ children }: Props) { export default function Voice({ children }: Props) {
const revoltClient = useContext(AppContext);
const [client, setClient] = useState<VoiceClient | undefined>(undefined); const [client, setClient] = useState<VoiceClient | undefined>(undefined);
const [state, setState] = useState<VoiceState>({ const [state, setState] = useState<VoiceState>({
status: VoiceStatus.LOADING, status: VoiceStatus.LOADING,
participants: new Map(), participants: new Map(),
}); });
function setStatus(status: VoiceStatus, roomId?: string) { const setStatus = useCallback(
setState({ (status: VoiceStatus, roomId?: string) => {
status, setState({
roomId: roomId ?? client?.roomId, status,
participants: client?.participants ?? new Map(), roomId: roomId ?? client?.roomId,
}); participants: client?.participants ?? new Map(),
} });
},
[client?.participants, client?.roomId],
);
useEffect(() => { useEffect(() => {
import("../lib/vortex/VoiceClient") import("../lib/vortex/VoiceClient")
@ -76,7 +84,7 @@ export default function Voice({ children }: Props) {
console.error("Failed to load voice library!", err); console.error("Failed to load voice library!", err);
setStatus(VoiceStatus.UNAVAILABLE); setStatus(VoiceStatus.UNAVAILABLE);
}); });
}, []); }, [setStatus]);
const isConnecting = useRef(false); const isConnecting = useRef(false);
const operations: VoiceOperations = useMemo(() => { const operations: VoiceOperations = useMemo(() => {
@ -158,7 +166,7 @@ export default function Voice({ children }: Props) {
return client?.stopProduce(type); return client?.stopProduce(type);
}, },
}; };
}, [client]); }, [client, setStatus]);
const playSound = useContext(SoundContext); const playSound = useContext(SoundContext);
@ -200,7 +208,7 @@ export default function Voice({ children }: Props) {
client.removeListener("userStopProduce", stateUpdate); client.removeListener("userStopProduce", stateUpdate);
client.removeListener("close", stateUpdate); client.removeListener("close", stateUpdate);
}; };
}, [client, state]); }, [client, state, playSound, setStatus]);
return ( return (
<VoiceContext.Provider value={state}> <VoiceContext.Provider value={state}>

View file

@ -89,13 +89,16 @@ export type Screen =
}; };
export const IntermediateContext = createContext({ export const IntermediateContext = createContext({
screen: { id: "none" } as Screen, screen: { id: "none" },
focusTaken: false, focusTaken: false,
}); });
export const IntermediateActionsContext = createContext({ export const IntermediateActionsContext = createContext<{
openScreen: (screen: Screen) => {}, openScreen: (screen: Screen) => void;
writeClipboard: (text: string) => {}, writeClipboard: (text: string) => void;
}>({
openScreen: null!,
writeClipboard: null!,
}); });
interface Props { interface Props {
@ -130,12 +133,20 @@ export default function Intermediate(props: Props) {
const navigate = (path: string) => history.push(path); const navigate = (path: string) => history.push(path);
const subs = [ const subs = [
internalSubscribe("Intermediate", "openProfile", openProfile), internalSubscribe(
internalSubscribe("Intermediate", "navigate", navigate), "Intermediate",
"openProfile",
openProfile as (...args: unknown[]) => void,
),
internalSubscribe(
"Intermediate",
"navigate",
navigate as (...args: unknown[]) => void,
),
]; ];
return () => subs.map((unsub) => unsub()); return () => subs.map((unsub) => unsub());
}, []); }, [history]);
return ( return (
<IntermediateContext.Provider value={value}> <IntermediateContext.Provider value={value}>

View file

@ -12,7 +12,7 @@ import { SignedOutModal } from "./modals/SignedOut";
export interface Props { export interface Props {
screen: Screen; screen: Screen;
openScreen: (id: any) => void; openScreen: (screen: Screen) => void;
} }
export default function Modals({ screen, openScreen }: Props) { export default function Modals({ screen, openScreen }: Props) {

View file

@ -29,7 +29,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
setLoading(true); setLoading(true);
callback(username, true) callback(username, true)
.then(() => onClose()) .then(() => onClose())
.catch((err: any) => { .catch((err: unknown) => {
setError(takeError(err)); setError(takeError(err));
setLoading(false); setLoading(false);
}); });

View file

@ -238,7 +238,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
.then((code) => setCode(code)) .then((code) => setCode(code))
.catch((err) => setError(takeError(err))) .catch((err) => setError(takeError(err)))
.finally(() => setProcessing(false)); .finally(() => setProcessing(false));
}, []); }, [props.target]);
return ( return (
<PromptModal <PromptModal

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn"; import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn";
import { EmbedImage } from "revolt-api/types/January"; import { EmbedImage } from "revolt-api/types/January";

View file

@ -85,11 +85,13 @@ export function ModifyAccountModal({ onClose, field }: Props) {
]}> ]}>
{/* Preact / React typing incompatabilities */} {/* Preact / React typing incompatabilities */}
<form <form
onSubmit={ onSubmit={(e) => {
e.preventDefault();
handleSubmit( handleSubmit(
onSubmit, onSubmit,
) as JSX.GenericEventHandler<HTMLFormElement> // eslint-disable-next-line @typescript-eslint/no-explicit-any
}> )(e as any);
}}>
{field === "email" && ( {field === "email" && (
<FormField <FormField
type="email" type="email"

View file

@ -42,6 +42,7 @@ export function UserPicker(props: Props) {
) )
.map((x) => ( .map((x) => (
<UserCheckbox <UserCheckbox
key={x._id}
user={x} user={x}
checked={selected.includes(x._id)} checked={selected.includes(x._id)}
onChange={(v) => { onChange={(v) => {

View file

@ -20,7 +20,6 @@ import Preloader from "../../../components/ui/Preloader";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
import { import {
AppContext,
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient, useClient,
@ -30,7 +29,7 @@ import { useIntermediate } from "../Intermediate";
interface Props { interface Props {
user_id: string; user_id: string;
dummy?: boolean; dummy?: boolean;
onClose: () => void; onClose?: () => void;
dummyProfile?: Profile; dummyProfile?: Profile;
} }
@ -60,7 +59,7 @@ export const UserProfile = observer(
const user = client.users.get(user_id); const user = client.users.get(user_id);
if (!user) { if (!user) {
useEffect(onClose, []); if (onClose) useEffect(onClose, []);
return null; return null;
} }
@ -76,7 +75,7 @@ export const UserProfile = observer(
if (!user_id) return; if (!user_id) return;
if (typeof profile !== "undefined") setProfile(undefined); if (typeof profile !== "undefined") setProfile(undefined);
if (typeof mutual !== "undefined") setMutual(undefined); if (typeof mutual !== "undefined") setMutual(undefined);
}, [user_id]); }, [user_id, mutual, profile]);
if (dummy) { if (dummy) {
useLayoutEffect(() => { useLayoutEffect(() => {
@ -93,7 +92,7 @@ export const UserProfile = observer(
setMutual(null); setMutual(null);
user.fetchMutual().then(setMutual); user.fetchMutual().then(setMutual);
} }
}, [mutual, status]); }, [mutual, status, dummy, user]);
useEffect(() => { useEffect(() => {
if (dummy) return; if (dummy) return;
@ -104,12 +103,10 @@ export const UserProfile = observer(
setProfile(null); setProfile(null);
if (user.permission & UserPermission.ViewProfile) { if (user.permission & UserPermission.ViewProfile) {
user.fetchProfile() user.fetchProfile().then(setProfile);
.then(setProfile)
.catch(() => {});
} }
} }
}, [profile, status]); }, [profile, status, dummy, user]);
const backgroundURL = const backgroundURL =
profile && profile &&
@ -157,7 +154,7 @@ export const UserProfile = observer(
}> }>
<IconButton <IconButton
onClick={() => { onClick={() => {
onClose(); onClose?.();
history.push(`/open/${user_id}`); history.push(`/open/${user_id}`);
}}> }}>
<Envelope size={30} /> <Envelope size={30} />
@ -168,7 +165,7 @@ export const UserProfile = observer(
{user.relationship === RelationshipStatus.User && ( {user.relationship === RelationshipStatus.User && (
<IconButton <IconButton
onClick={() => { onClick={() => {
onClose(); onClose?.();
if (dummy) return; if (dummy) return;
history.push(`/settings/profile`); history.push(`/settings/profile`);
}}> }}>

View file

@ -1,4 +1,4 @@
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";
@ -147,6 +147,7 @@ export function FileUploader(props: Props) {
} }
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) {
@ -210,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") {

View file

@ -1,4 +1,3 @@
import { autorun, reaction } from "mobx";
import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Presence, RelationshipStatus } from "revolt-api/types/Users"; import { Presence, RelationshipStatus } from "revolt-api/types/Users";
import { SYSTEM_USER_ID } from "revolt.js"; import { SYSTEM_USER_ID } from "revolt.js";
@ -6,7 +5,7 @@ import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js/dist/maps/Users"; 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";
@ -52,191 +51,206 @@ 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_id === client.user!._id) return; async (msg: Message) => {
if (msg.channel_id === channel_id && document.hasFocus()) return; if (msg.author_id === client.user!._id) return;
if (client.user!.status?.presence === Presence.Busy) return; if (msg.channel_id === channel_id && document.hasFocus()) return;
if (msg.author?.relationship === RelationshipStatus.Blocked) return; if (client.user!.status?.presence === Presence.Busy) return;
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
const notifState = getNotificationState(notifs, msg.channel!); const notifState = getNotificationState(notifs, msg.channel!);
if (!shouldNotify(notifState, msg, client.user!._id)) return; if (!shouldNotify(notifState, msg, client.user!._id)) return;
playSound("message"); playSound("message");
if (!showNotification) return; if (!showNotification) return;
let title; let title;
switch (msg.channel?.channel_type) { switch (msg.channel?.channel_type) {
case "SavedMessages": case "SavedMessages":
return; return;
case "DirectMessage": case "DirectMessage":
title = `@${msg.author?.username}`; title = `@${msg.author?.username}`;
break; break;
case "Group": case "Group":
if (msg.author?._id === SYSTEM_USER_ID) { if (msg.author?._id === SYSTEM_USER_ID) {
title = msg.channel.name; title = msg.channel.name;
} else { } else {
title = `@${msg.author?.username} - ${msg.channel.name}`; title = `@${msg.author?.username} - ${msg.channel.name}`;
}
break;
case "TextChannel":
title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
break;
default:
title = msg.channel?._id;
break;
}
let image;
if (msg.attachments) {
const imageAttachment = msg.attachments.find(
(x) => x.metadata.type === "Image",
);
if (imageAttachment) {
image = client.generateFileURL(imageAttachment, {
max_side: 720,
});
} }
break;
case "TextChannel":
title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
break;
default:
title = msg.channel?._id;
break;
}
let image;
if (msg.attachments) {
const imageAttachment = msg.attachments.find(
(x) => x.metadata.type === "Image",
);
if (imageAttachment) {
image = client.generateFileURL(imageAttachment, {
max_side: 720,
});
} }
}
let body, icon; let body, icon;
if (typeof msg.content === "string") { if (typeof msg.content === "string") {
body = client.markdownToText(msg.content); body = client.markdownToText(msg.content);
icon = msg.author?.generateAvatarURL({ max_side: 256 }); icon = msg.author?.generateAvatarURL({ max_side: 256 });
} else { } else {
const users = client.users; const users = client.users;
switch (msg.content.type) { switch (msg.content.type) {
case "user_added": case "user_added":
case "user_remove": case "user_remove":
{ {
let user = users.get(msg.content.id); const user = users.get(msg.content.id);
body = translate( body = translate(
`app.main.channel.system.${ `app.main.channel.system.${
msg.content.type === "user_added" msg.content.type === "user_added"
? "added_by" ? "added_by"
: "removed_by" : "removed_by"
}`, }`,
{ {
user: user?.username, user: user?.username,
other_user: users.get(msg.content.by)?.username, other_user: users.get(msg.content.by)
}, ?.username,
); },
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
{
let 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":
{
let 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":
{
let 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,
});
if (notif) {
notif.addEventListener("click", () => {
window.focus();
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 { 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_id] = notif; if (notif) {
notif.addEventListener( notif.addEventListener("click", () => {
"close", window.focus();
() => delete notifications[msg.channel_id], const id = msg.channel_id;
); if (id !== channel_id) {
} const channel = client.channels.get(id);
} if (channel) {
if (channel.channel_type === "TextChannel") {
async function relationship(user: User) { history.push(
if (client.user?.status?.presence === Presence.Busy) return; `/server/${channel.server_id}/channel/${id}`,
if (!showNotification) return; );
} else {
let event; history.push(`/channel/${id}`);
switch (user.relationship) { }
case RelationshipStatus.Incoming: }
event = translate("notifications.sent_request", { }
person: user.username,
}); });
break;
case RelationshipStatus.Friend:
event = translate("notifications.now_friends", {
person: user.username,
});
break;
default:
return;
}
const notif = await createNotification(event, { notifications[msg.channel_id] = notif;
icon: user.generateAvatarURL({ max_side: 256 }), notif.addEventListener(
badge: "/assets/icons/android-chrome-512x512.png", "close",
timestamp: +new Date(), () => delete notifications[msg.channel_id],
}); );
}
},
[
history,
showNotification,
translate,
channel_id,
client,
notifs,
playSound,
],
);
notif?.addEventListener("click", () => { const relationship = useCallback(
history.push(`/friends`); async (user: User) => {
}); if (client.user?.status?.presence === Presence.Busy) return;
} if (!showNotification) return;
let event;
switch (user.relationship) {
case RelationshipStatus.Incoming:
event = translate("notifications.sent_request", {
person: user.username,
});
break;
case RelationshipStatus.Friend:
event = translate("notifications.now_friends", {
person: user.username,
});
break;
default:
return;
}
const notif = await createNotification(event, {
icon: user.generateAvatarURL({ max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(),
});
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
},
[client.user?.status?.presence, history, showNotification, translate],
);
useEffect(() => { useEffect(() => {
client.addListener("message", message); client.addListener("message", message);
@ -246,7 +260,16 @@ function Notifier({ options, notifs }: Props) {
client.removeListener("message", message); client.removeListener("message", message);
client.removeListener("user/relationship", 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() {

View file

@ -1,5 +1,4 @@
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";
@ -58,29 +57,6 @@ function Context({ auth, children }: Props) {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
let db;
try {
// Match sw.ts#L23
db = await openDB("state", 3, {
upgrade(db) {
for (const 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,
@ -146,11 +122,11 @@ function Context({ auth, children }: Props) {
ready: () => ready: () =>
operations.loggedIn() && typeof client.user !== "undefined", operations.loggedIn() && typeof client.user !== "undefined",
}; };
}, [client, auth.active]); }, [client, auth.active, openScreen]);
useEffect( useEffect(
() => registerEvents({ operations }, setStatus, client), () => registerEvents({ operations }, setStatus, client),
[client], [client, operations],
); );
useEffect(() => { useEffect(() => {
@ -203,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) {

View file

@ -37,7 +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]);
return null; return null;
} }

View file

@ -5,7 +5,7 @@ import isEqual from "lodash.isequal";
import { UserSettings } from "revolt-api/types/Sync"; 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,7 +28,7 @@ type Props = {
notifications: Notifications; notifications: Notifications;
}; };
const lastValues: { [key in SyncKeys]?: any } = {}; const lastValues: { [key in SyncKeys]?: unknown } = {};
export function mapSync( export function mapSync(
packet: UserSettings, packet: UserSettings,
@ -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(
const 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,
); );
} },
[client],
);
const disabled = props.sync.disabled ?? []; const disabled = useMemo(
() => props.sync.disabled ?? [],
[props.sync.disabled],
);
for (const [key, object] of [ 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,7 +120,7 @@ function SyncManager(props: Props) {
} }
lastValues[key] = object; lastValues[key] = object;
}, [disabled, object]); }, [key, syncChange, disabled, object]);
} }
useEffect(() => { useEffect(() => {
@ -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;
} }

View file

@ -8,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) {
@ -34,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),
@ -74,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);
@ -87,10 +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);
}
const online = () => { const online = () => {
if (operations.ready()) { if (operations.ready()) {
setStatus(ClientStatus.RECONNECTING); setStatus(ClientStatus.RECONNECTING);

View file

@ -1,10 +1,10 @@
import { Client } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
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;
const id = type; const id = type;

4
src/globals.d.ts vendored
View file

@ -18,10 +18,12 @@ declare interface Window {
relaunch(); relaunch();
getConfig(): NativeConfig; getConfig(): NativeConfig;
set(key: keyof NativeConfig, value: any); set(key: keyof NativeConfig, value: unknown);
getAutoStart(): Promise<boolean>; getAutoStart(): Promise<boolean>;
enableAutoStart(): Promise<void>; enableAutoStart(): Promise<void>;
disableAutoStart(): Promise<void>; disableAutoStart(): Promise<void>;
}; };
} }
declare const Fragment = preact.Fragment;

View file

@ -25,7 +25,6 @@ import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js/dist/maps/Users";
import { import {
ContextMenu,
ContextMenuWithData, ContextMenuWithData,
MenuItem, MenuItem,
openContextMenu, openContextMenu,
@ -42,12 +41,11 @@ 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,
useClient,
} from "../context/revoltjs/RevoltClient"; } from "../context/revoltjs/RevoltClient";
import { takeError } from "../context/revoltjs/util"; import { takeError } from "../context/revoltjs/util";
@ -179,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,
@ -369,7 +367,8 @@ function ContextMenus(props: Props) {
case "clear_status": case "clear_status":
{ {
const { text, ...status } = client.user?.status ?? {}; const { text: _text, ...status } =
client.user?.status ?? {};
await client.users.edit({ status }); await client.users.edit({ status });
} }
break; break;
@ -382,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":
@ -596,8 +595,11 @@ function ContextMenus(props: Props) {
} }
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);
} }
} }
@ -968,6 +970,7 @@ function ContextMenus(props: Props) {
const 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,
@ -987,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,

View file

@ -1,3 +1,4 @@
/* 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 } = {};

View file

@ -10,7 +10,7 @@ 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;
@ -63,8 +63,6 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
lineHeight, lineHeight,
hideBorder, hideBorder,
forceFocus, forceFocus,
children,
as,
onChange, onChange,
...textAreaProps ...textAreaProps
} = props; } = props;
@ -81,7 +79,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
useEffect(() => { useEffect(() => {
if (isTouchscreenDevice) return; if (isTouchscreenDevice) return;
autoFocus && ref.current && 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 ?? "");
@ -114,7 +112,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
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; if (!ref.current) return;
@ -124,8 +122,12 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
} }
} }
return internalSubscribe("TextArea", "focus", focus); return internalSubscribe(
}, [ref]); "TextArea",
"focus",
focus as (...args: unknown[]) => void,
);
}, [props.id, ref]);
return ( return (
<Container> <Container>

View file

@ -1,7 +1,7 @@
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);

View file

@ -1,8 +1,8 @@
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.

View file

@ -5,13 +5,13 @@ 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);
} }

View file

@ -47,7 +47,7 @@ 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 (const key of path) { for (const key of path) {
// @ts-expect-error // @ts-expect-error TODO: lazy
entry = entry[key]; entry = entry[key];
} }
@ -56,8 +56,12 @@ 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() { export function useDictionary() {

4
src/lib/js.ts Normal file
View file

@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-empty-function */
export const noop = () => {};
export const noopAsync = async () => {};
/* eslint-enable @typescript-eslint/no-empty-function */

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import EventEmitter3 from "eventemitter3"; import EventEmitter3 from "eventemitter3";
import { Client } from "revolt.js"; import { Client } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js/dist/maps/Messages";
@ -123,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }
@ -167,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 {
window window
.getComputedStyle(child) .getComputedStyle(child)
.marginTop.slice(0, -2), .marginTop.slice(0, -2),
10,
); );
} }
} }

View file

@ -1,3 +1,4 @@
import { noopAsync } from "../../js";
import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton"; import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
import { RendererRoutines } from "../types"; import { RendererRoutines } from "../types";
@ -65,7 +66,7 @@ export const SimpleRenderer: RendererRoutines = {
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE }, { type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
); );
}, },
edit: async () => {}, edit: noopAsync,
delete: async (renderer, id) => { delete: async (renderer, id) => {
const channel = renderer.channel; const channel = renderer.channel;
if (!channel) return; if (!channel) return;

View file

@ -1,6 +1,7 @@
export const stopPropagation = ( export const stopPropagation = (
ev: JSX.TargetedMouseEvent<HTMLElement>, ev: JSX.TargetedMouseEvent<HTMLElement>,
_consume?: any, // eslint-disable-next-line
_consume?: unknown,
) => { ) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();

View file

@ -20,6 +20,7 @@ interface SignalingEvents {
open: (event: Event) => void; open: (event: Event) => void;
close: (event: CloseEvent) => void; close: (event: CloseEvent) => void;
error: (event: Event) => void; error: (event: Event) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: (data: any) => void; data: (data: any) => void;
} }
@ -87,6 +88,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
entry(json); entry(json);
} }
/* eslint-disable @typescript-eslint/no-explicit-any */
sendRequest(type: string, data?: any): Promise<any> { sendRequest(type: string, data?: any): Promise<any> {
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN) if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
return Promise.reject({ error: WSErrorCode.NotConnected }); return Promise.reject({ error: WSErrorCode.NotConnected });
@ -124,6 +126,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
this.index++; this.index++;
}); });
} }
/* eslint-enable @typescript-eslint/no-explicit-any */
authenticate(token: string, roomId: string): Promise<AuthenticationResult> { authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
return this.sendRequest(WSCommandType.Authenticate, { token, roomId }); return this.sendRequest(WSCommandType.Authenticate, { token, roomId });

View file

@ -114,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
this.signaling.on( this.signaling.on(
"error", "error",
(error) => { () => {
this.emit("error", new Error("Signaling error")); this.emit("error", new Error("Signaling error"));
}, },
this, this,

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -44,7 +45,7 @@ export default function Open() {
return; return;
} }
let user = client.users.get(id); const user = client.users.get(id);
if (user) { if (user) {
const channel: string | undefined = [ const channel: string | undefined = [
...client.channels.values(), ...client.channels.values(),
@ -68,7 +69,7 @@ export default function Open() {
} }
history.push("/"); history.push("/");
}, []); });
return ( return (
<Header placement="primary"> <Header placement="primary">

View file

@ -16,7 +16,7 @@ export function App() {
<Context> <Context>
<Masks /> <Masks />
{/* {/*
// @ts-expect-error */} // @ts-expect-error typings mis-match between preact... and preact? */}
<Suspense fallback={<Preloader type="spinner" />}> <Suspense fallback={<Preloader type="spinner" />}>
<Switch> <Switch>
<Route path="/login"> <Route path="/login">

View file

@ -60,11 +60,11 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
type="channel" type="channel"
channel={channel} channel={channel}
gated={ gated={
(channel.channel_type === "TextChannel" || !!(
channel.channel_type === "Group") && (channel.channel_type === "TextChannel" ||
channel.name?.includes("nsfw") channel.channel_type === "Group") &&
? true channel.name?.includes("nsfw")
: false )
}> }>
<ChannelHeader <ChannelHeader
channel={channel} channel={channel}
@ -110,7 +110,7 @@ function VoiceChannel({ channel }: { channel: ChannelI }) {
); );
} }
export default function () { export default function ChannelComponent() {
const { channel } = useParams<{ channel: string }>(); const { channel } = useParams<{ channel: string }>();
return <Channel id={channel} key={channel} />; return <Channel id={channel} key={channel} />;
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { import {
UserPlus, UserPlus,
Cog, Cog,
@ -9,15 +10,12 @@ import { useHistory } from "react-router-dom";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { import {
VoiceContext, VoiceContext,
VoiceOperationsContext, VoiceOperationsContext,
VoiceStatus, VoiceStatus,
} from "../../../context/Voice"; } from "../../../context/Voice";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import UpdateIndicator from "../../../components/common/UpdateIndicator"; import UpdateIndicator from "../../../components/common/UpdateIndicator";
import IconButton from "../../../components/ui/IconButton"; import IconButton from "../../../components/ui/IconButton";

View file

@ -5,6 +5,7 @@ import useResizeObserver from "use-resize-observer";
import { createContext } from "preact"; import { createContext } from "preact";
import { import {
useCallback,
useContext, useContext,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
@ -74,7 +75,7 @@ export function MessageArea({ id }: Props) {
// ? useRef to avoid re-renders // ? useRef to avoid re-renders
const scrollState = useRef<ScrollState>({ type: "Free" }); const scrollState = useRef<ScrollState>({ type: "Free" });
const setScrollState = (v: ScrollState) => { const setScrollState = useCallback((v: ScrollState) => {
if (v.type === "StayAtBottom") { if (v.type === "StayAtBottom") {
if (scrollState.current.type === "Bottom" || atBottom()) { if (scrollState.current.type === "Bottom" || atBottom()) {
scrollState.current = { scrollState.current = {
@ -131,7 +132,7 @@ export function MessageArea({ id }: Props) {
setScrollState({ type: "Free" }); setScrollState({ type: "Free" });
} }
}); });
}; }, []);
// ? Determine if we are at the bottom of the scroll container. // ? Determine if we are at the bottom of the scroll container.
// -> https://stackoverflow.com/a/44893438 // -> https://stackoverflow.com/a/44893438
@ -151,7 +152,7 @@ export function MessageArea({ id }: Props) {
return internalSubscribe("MessageArea", "jump_to_bottom", () => return internalSubscribe("MessageArea", "jump_to_bottom", () =>
setScrollState({ type: "ScrollToBottom" }), setScrollState({ type: "ScrollToBottom" }),
); );
}, []); }, [setScrollState]);
// ? Handle events from renderer. // ? Handle events from renderer.
useEffect(() => { useEffect(() => {
@ -163,12 +164,13 @@ export function MessageArea({ id }: Props) {
SingletonMessageRenderer.addListener("scroll", setScrollState); SingletonMessageRenderer.addListener("scroll", setScrollState);
return () => return () =>
SingletonMessageRenderer.removeListener("scroll", setScrollState); SingletonMessageRenderer.removeListener("scroll", setScrollState);
}, [scrollState]); }, [scrollState, setScrollState]);
// ? Load channel initially. // ? Load channel initially.
useEffect(() => { useEffect(() => {
if (message) return; if (message) return;
SingletonMessageRenderer.init(id); SingletonMessageRenderer.init(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); }, [id]);
// ? If message present or changes, load it as well. // ? If message present or changes, load it as well.
@ -184,6 +186,7 @@ export function MessageArea({ id }: Props) {
history.push(`/channel/${id}`); history.push(`/channel/${id}`);
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message]); }, [message]);
// ? If we are waiting for network, try again. // ? If we are waiting for network, try again.
@ -203,11 +206,14 @@ export function MessageArea({ id }: Props) {
SingletonMessageRenderer.markStale(); SingletonMessageRenderer.markStale();
break; break;
} }
}, [status, state]); }, [id, status, state]);
// ? When the container is scrolled. // ? When the container is scrolled.
// ? Also handle StayAtBottom // ? Also handle StayAtBottom
useEffect(() => { useEffect(() => {
const current = ref.current;
if (!current) return;
async function onScroll() { async function onScroll() {
if (scrollState.current.type === "Free" && atBottom()) { if (scrollState.current.type === "Free" && atBottom()) {
setScrollState({ type: "Bottom" }); setScrollState({ type: "Bottom" });
@ -221,12 +227,15 @@ export function MessageArea({ id }: Props) {
} }
} }
ref.current?.addEventListener("scroll", onScroll); current.addEventListener("scroll", onScroll);
return () => ref.current?.removeEventListener("scroll", onScroll); return () => current.removeEventListener("scroll", onScroll);
}, [ref, scrollState]); }, [ref, scrollState, setScrollState]);
// ? Top and bottom loaders. // ? Top and bottom loaders.
useEffect(() => { useEffect(() => {
const current = ref.current;
if (!current) return;
async function onScroll() { async function onScroll() {
if (atTop(100)) { if (atTop(100)) {
SingletonMessageRenderer.loadTop(ref.current!); SingletonMessageRenderer.loadTop(ref.current!);
@ -237,12 +246,12 @@ export function MessageArea({ id }: Props) {
} }
} }
ref.current?.addEventListener("scroll", onScroll); current.addEventListener("scroll", onScroll);
return () => ref.current?.removeEventListener("scroll", onScroll); return () => current.removeEventListener("scroll", onScroll);
}, [ref]); }, [ref]);
// ? Scroll down whenever the message area resizes. // ? Scroll down whenever the message area resizes.
function stbOnResize() { const stbOnResize = useCallback(() => {
if (!atBottom() && scrollState.current.type === "Bottom") { if (!atBottom() && scrollState.current.type === "Bottom") {
animateScroll.scrollToBottom({ animateScroll.scrollToBottom({
container: ref.current, container: ref.current,
@ -251,18 +260,18 @@ export function MessageArea({ id }: Props) {
setScrollState({ type: "Bottom" }); setScrollState({ type: "Bottom" });
} }
} }, [setScrollState]);
// ? Scroll down when container resized. // ? Scroll down when container resized.
useLayoutEffect(() => { useLayoutEffect(() => {
stbOnResize(); stbOnResize();
}, [height]); }, [stbOnResize, height]);
// ? Scroll down whenever the window resizes. // ? Scroll down whenever the window resizes.
useLayoutEffect(() => { useLayoutEffect(() => {
document.addEventListener("resize", stbOnResize); document.addEventListener("resize", stbOnResize);
return () => document.removeEventListener("resize", stbOnResize); return () => document.removeEventListener("resize", stbOnResize);
}, [ref, scrollState]); }, [ref, scrollState, stbOnResize]);
// ? Scroll to bottom when pressing 'Escape'. // ? Scroll to bottom when pressing 'Escape'.
useEffect(() => { useEffect(() => {
@ -275,7 +284,7 @@ export function MessageArea({ id }: Props) {
document.body.addEventListener("keyup", keyUp); document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp); return () => document.body.removeEventListener("keyup", keyUp);
}, [ref, focusTaken]); }, [id, ref, focusTaken]);
return ( return (
<MessageAreaWidthContext.Provider <MessageAreaWidthContext.Provider

View file

@ -10,7 +10,6 @@ import {
IntermediateContext, IntermediateContext,
useIntermediate, useIntermediate,
} from "../../../context/intermediate/Intermediate"; } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import AutoComplete, { import AutoComplete, {
useAutoComplete, useAutoComplete,
@ -79,7 +78,7 @@ export default function MessageEditor({ message, finish }: Props) {
document.body.addEventListener("keyup", keyUp); document.body.addEventListener("keyup", keyUp);
return () => document.body.removeEventListener("keyup", keyUp); return () => document.body.removeEventListener("keyup", keyUp);
}, [focusTaken]); }, [focusTaken, finish]);
const { const {
onChange, onChange,

View file

@ -1,14 +1,14 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { X } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import { RelationshipStatus } from "revolt-api/types/Users"; import { RelationshipStatus } from "revolt-api/types/Users";
import { SYSTEM_USER_ID } from "revolt.js"; import { SYSTEM_USER_ID } from "revolt.js";
import { Message as MessageObject } from "revolt.js/dist/maps/Messages";
import { Message as MessageI } from "revolt.js/dist/maps/Messages"; import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import styled from "styled-components"; import styled from "styled-components";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { useContext, useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter"; import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
import { RenderState } from "../../../lib/renderer/types"; import { RenderState } from "../../../lib/renderer/types";
@ -17,7 +17,7 @@ import { connectState } from "../../../redux/connector";
import { QueuedMessage } from "../../../redux/reducers/queue"; import { QueuedMessage } from "../../../redux/reducers/queue";
import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
import { AppContext, useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import Message from "../../../components/common/messaging/Message"; import Message from "../../../components/common/messaging/Message";
import { SystemMessage } from "../../../components/common/messaging/SystemMessage"; import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
@ -76,10 +76,10 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
]; ];
return () => subs.forEach((unsub) => unsub()); return () => subs.forEach((unsub) => unsub());
}, [state.messages]); }, [state.messages, state.type, userId]);
let render: Children[] = [], const render: Children[] = [];
previous: MessageObject | undefined; let previous: MessageI | undefined;
if (state.atTop) { if (state.atTop) {
render.push(<ConversationStart id={id} />); render.push(<ConversationStart id={id} />);
@ -148,30 +148,30 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
highlight={highlight === message._id} highlight={highlight === message._id}
/>, />,
); );
} else if (
message.author?.relationship === RelationshipStatus.Blocked
) {
blocked++;
} else { } else {
if (message.author?.relationship === RelationshipStatus.Blocked) { if (blocked > 0) pushBlocked();
blocked++;
} else {
if (blocked > 0) pushBlocked();
render.push( render.push(
<Message <Message
message={message} message={message}
key={message._id} key={message._id}
head={head} head={head}
content={ content={
editing === message._id ? ( editing === message._id ? (
<MessageEditor <MessageEditor
message={message} message={message}
finish={stopEditing} finish={stopEditing}
/> />
) : undefined ) : undefined
} }
attachContext attachContext
highlight={highlight === message._id} highlight={highlight === message._id}
/>, />,
); );
}
} }
previous = message; previous = message;
@ -191,7 +191,7 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
previous = { previous = {
_id: msg.id, _id: msg.id,
author_id: userId!, author_id: userId!,
} as any; } as MessageI;
} }
render.push( render.push(

View file

@ -1,5 +1,4 @@
import { BarChart } from "@styled-icons/boxicons-regular"; import { BarChart } from "@styled-icons/boxicons-regular";
import { observable } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import styled from "styled-components"; import styled from "styled-components";

View file

@ -15,10 +15,6 @@ import { stopPropagation } from "../../lib/stopPropagation";
import { VoiceOperationsContext } from "../../context/Voice"; import { VoiceOperationsContext } from "../../context/Voice";
import { useIntermediate } from "../../context/intermediate/Intermediate"; import { useIntermediate } from "../../context/intermediate/Intermediate";
import {
AppContext,
OperationsContext,
} from "../../context/revoltjs/RevoltClient";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import UserStatus from "../../components/common/user/UserStatus"; import UserStatus from "../../components/common/user/UserStatus";

View file

@ -1,8 +1,4 @@
import { import { ChevronRight } from "@styled-icons/boxicons-regular";
ChevronDown,
ChevronRight,
ListPlus,
} from "@styled-icons/boxicons-regular";
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid"; import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { RelationshipStatus, Presence } from "revolt-api/types/Users"; import { RelationshipStatus, Presence } from "revolt-api/types/Users";
@ -68,7 +64,9 @@ export default observer(() => {
] as [string, User[], string][]; ] as [string, User[], string][];
const incoming = lists[0][1]; const incoming = lists[0][1];
const userlist: Children[] = incoming.map((x) => <b>{x.username}</b>); const userlist: Children[] = incoming.map((x) => (
<b key={x._id}>{x.username}</b>
));
for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", "); for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0; const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
@ -195,6 +193,7 @@ export default observer(() => {
return ( return (
<CollapsibleSection <CollapsibleSection
key={section_id}
id={`friends_${section_id}`} id={`friends_${section_id}`}
defaultValue={true} defaultValue={true}
sticky sticky

View file

@ -1,4 +1,3 @@
import { Coffee } from "@styled-icons/boxicons-regular";
import { Home as HomeIcon } from "@styled-icons/boxicons-solid"; import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";

View file

@ -45,7 +45,7 @@ export default function Invite() {
.then((data) => setInvite(data)) .then((data) => setInvite(data))
.catch((err) => setError(takeError(err))); .catch((err) => setError(takeError(err)));
} }
}, [status]); }, [client, code, invite, status]);
if (typeof invite === "undefined") { if (typeof invite === "undefined") {
return ( return (
@ -128,7 +128,7 @@ export default function Invite() {
} }
const dispose = autorun(() => { const dispose = autorun(() => {
let server = client.servers.get( const server = client.servers.get(
invite.server_id, invite.server_id,
); );

View file

@ -1,3 +1,5 @@
import { UseFormMethods } from "react-hook-form";
import { Text, Localizer } from "preact-i18n"; import { Text, Localizer } from "preact-i18n";
import InputBox from "../../components/ui/InputBox"; import InputBox from "../../components/ui/InputBox";
@ -6,7 +8,7 @@ import Overline from "../../components/ui/Overline";
interface Props { interface Props {
type: "email" | "username" | "password" | "invite" | "current_password"; type: "email" | "username" | "password" | "invite" | "current_password";
showOverline?: boolean; showOverline?: boolean;
register: Function; register: UseFormMethods["register"];
error?: string; error?: string;
name?: string; name?: string;
} }
@ -27,9 +29,11 @@ export default function FormField({
)} )}
<Localizer> <Localizer>
<InputBox <InputBox
// Styled uses React typing while we use Preact placeholder={
// this leads to inconsistances where things need to be typed oddly (
placeholder={(<Text id={`login.enter.${type}`} />) as any} <Text id={`login.enter.${type}`} />
) as unknown as string
}
name={ name={
type === "current_password" ? "password" : name ?? type type === "current_password" ? "password" : name ?? type
} }

View file

@ -20,7 +20,7 @@ export function CaptchaBlock(props: CaptchaProps) {
if (!client.configuration?.features.captcha.enabled) { if (!client.configuration?.features.captcha.enabled) {
props.onSuccess(); props.onSuccess();
} }
}, []); }, [client.configuration?.features.captcha.enabled, props]);
if (!client.configuration?.features.captcha.enabled) if (!client.configuration?.features.captcha.enabled)
return <Preloader type="spinner" />; return <Preloader type="spinner" />;

View file

@ -63,7 +63,7 @@ export function Form({ page, callback }: Props) {
setGlobalError(undefined); setGlobalError(undefined);
setLoading(true); setLoading(true);
function onError(err: any) { function onError(err: unknown) {
setLoading(false); setLoading(false);
const error = takeError(err); const error = takeError(err);

View file

@ -1,5 +1,5 @@
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular"; import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
import { Route, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -16,7 +16,9 @@ export default function ChannelSettings() {
const { channel: cid } = useParams<{ channel: string }>(); const { channel: cid } = useParams<{ channel: string }>();
const client = useClient(); const client = useClient();
const history = useHistory();
const channel = client.channels.get(cid); const channel = client.channels.get(cid);
if (!channel) return null; if (!channel) return null;
if ( if (
channel.channel_type === "SavedMessages" || channel.channel_type === "SavedMessages" ||
@ -24,7 +26,6 @@ export default function ChannelSettings() {
) )
return null; return null;
const history = useHistory();
function switchPage(to?: string) { function switchPage(to?: string) {
let base_url; let base_url;
switch (channel?.channel_type) { switch (channel?.channel_type) {
@ -67,18 +68,20 @@ export default function ChannelSettings() {
), ),
}, },
]} ]}
children={[ children={
<Route path="/server/:server/channel/:channel/settings/permissions"> <Switch>
<Permissions channel={channel} /> <Route path="/server/:server/channel/:channel/settings/permissions">
</Route>, <Permissions channel={channel} />
<Route path="/channel/:channel/settings/permissions"> </Route>
<Permissions channel={channel} /> <Route path="/channel/:channel/settings/permissions">
</Route>, <Permissions channel={channel} />
</Route>
<Route path="/"> <Route>
<Overview channel={channel} /> <Overview channel={channel} />
</Route>, </Route>
]} </Switch>
}
category="channel_pages" category="channel_pages"
switchPage={switchPage} switchPage={switchPage}
defaultPage="overview" defaultPage="overview"

View file

@ -1,11 +1,11 @@
import { ArrowBack, X } from "@styled-icons/boxicons-regular"; import { ArrowBack, X } from "@styled-icons/boxicons-regular";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Switch, useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import styles from "./Settings.module.scss"; import styles from "./Settings.module.scss";
import classNames from "classnames"; import classNames from "classnames";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
@ -51,7 +51,7 @@ export function GenericSettings({
const { page } = useParams<{ page: string }>(); const { page } = useParams<{ page: string }>();
const [closing, setClosing] = useState(false); const [closing, setClosing] = useState(false);
function exitSettings() { const exitSettings = useCallback(() => {
if (history.length > 1) { if (history.length > 1) {
setClosing(true); setClosing(true);
@ -61,7 +61,7 @@ export function GenericSettings({
} else { } else {
history.push("/"); history.push("/");
} }
} }, [history]);
useEffect(() => { useEffect(() => {
function keyDown(e: KeyboardEvent) { function keyDown(e: KeyboardEvent) {
@ -72,7 +72,7 @@ export function GenericSettings({
document.body.addEventListener("keydown", keyDown); document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown); return () => document.body.removeEventListener("keydown", keyDown);
}, []); }, [exitSettings]);
return ( return (
<div <div
@ -158,7 +158,9 @@ export function GenericSettings({
<div className={styles.scrollbox}> <div className={styles.scrollbox}>
<div className={styles.contentcontainer}> <div className={styles.contentcontainer}>
{!isTouchscreenDevice && {!isTouchscreenDevice &&
!pages.find((x) => x.id === page && x.hideTitle) && ( !pages.find(
(x) => x.id === page && x.hideTitle,
) && (
<h1> <h1>
<Text <Text
id={`app.settings.${category}.${ id={`app.settings.${category}.${
@ -167,19 +169,20 @@ export function GenericSettings({
/> />
</h1> </h1>
)} )}
<Switch>{children}</Switch> {children}
</div> </div>
{!isTouchscreenDevice && ( {!isTouchscreenDevice && (
<div className={styles.action}> <div className={styles.action}>
<div onClick={exitSettings} className={styles.closeButton}> <div
<X size={28} /> onClick={exitSettings}
className={styles.closeButton}>
<X size={28} />
</div>
</div> </div>
</div>
)} )}
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
} }

View file

@ -1,7 +1,7 @@
import { ListUl, ListCheck, ListMinus } from "@styled-icons/boxicons-regular"; import { ListUl, ListCheck, ListMinus } from "@styled-icons/boxicons-regular";
import { XSquare, Share, Group } from "@styled-icons/boxicons-solid"; import { XSquare, Share, Group } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Route, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -77,34 +77,36 @@ export default observer(() => {
hideTitle: true, hideTitle: true,
}, },
]} ]}
children={[ children={
<Route path="/server/:server/settings/categories"> <Switch>
<Categories server={server} /> <Route path="/server/:server/settings/categories">
</Route>, <Categories server={server} />
<Route path="/server/:server/settings/members"> </Route>
<RequiresOnline> <Route path="/server/:server/settings/members">
<Members server={server} /> <RequiresOnline>
</RequiresOnline> <Members server={server} />
</Route>, </RequiresOnline>
<Route path="/server/:server/settings/invites"> </Route>
<RequiresOnline> <Route path="/server/:server/settings/invites">
<Invites server={server} /> <RequiresOnline>
</RequiresOnline> <Invites server={server} />
</Route>, </RequiresOnline>
<Route path="/server/:server/settings/bans"> </Route>
<RequiresOnline> <Route path="/server/:server/settings/bans">
<Bans server={server} /> <RequiresOnline>
</RequiresOnline> <Bans server={server} />
</Route>, </RequiresOnline>
<Route path="/server/:server/settings/roles"> </Route>
<RequiresOnline> <Route path="/server/:server/settings/roles">
<Roles server={server} /> <RequiresOnline>
</RequiresOnline> <Roles server={server} />
</Route>, </RequiresOnline>
<Route path="/"> </Route>
<Overview server={server} /> <Route>
</Route>, <Overview server={server} />
]} </Route>
</Switch>
}
category="server_pages" category="server_pages"
switchPage={switchPage} switchPage={switchPage}
defaultPage="overview" defaultPage="overview"

View file

@ -15,7 +15,7 @@ import {
User, User,
Megaphone, Megaphone,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { Route, useHistory } from "react-router-dom"; import { Route, Switch, useHistory } from "react-router-dom";
import { LIBRARY_VERSION } from "revolt.js"; import { LIBRARY_VERSION } from "revolt.js";
import styles from "./Settings.module.scss"; import styles from "./Settings.module.scss";
@ -120,103 +120,107 @@ export default function Settings() {
title: <Text id="app.settings.pages.feedback.title" />, title: <Text id="app.settings.pages.feedback.title" />,
}, },
]} ]}
children={[ children={
<Route path="/settings/profile"> <Switch>
<Profile /> <Route path="/settings/profile">
</Route>, <Profile />
<Route path="/settings/sessions"> </Route>
<RequiresOnline> <Route path="/settings/sessions">
<Sessions /> <RequiresOnline>
</RequiresOnline> <Sessions />
</Route>, </RequiresOnline>
<Route path="/settings/appearance"> </Route>
<Appearance /> <Route path="/settings/appearance">
</Route>, <Appearance />
<Route path="/settings/notifications"> </Route>
<Notifications /> <Route path="/settings/notifications">
</Route>, <Notifications />
<Route path="/settings/language"> </Route>
<Languages /> <Route path="/settings/language">
</Route>, <Languages />
<Route path="/settings/sync"> </Route>
<Sync /> <Route path="/settings/sync">
</Route>, <Sync />
<Route path="/settings/native"> </Route>
<Native /> <Route path="/settings/native">
</Route>, <Native />
<Route path="/settings/experiments"> </Route>
<ExperimentsPage /> <Route path="/settings/experiments">
</Route>, <ExperimentsPage />
<Route path="/settings/feedback"> </Route>
<Feedback /> <Route path="/settings/feedback">
</Route>, <Feedback />
<Route path="/"> </Route>
<Account /> <Route path="/" exact>
</Route>, <Account />
]} </Route>
</Switch>
}
defaultPage="account" defaultPage="account"
switchPage={switchPage} switchPage={switchPage}
category="pages" category="pages"
custom={[ custom={
<a <>
href="https://gitlab.insrt.uk/revolt" <a
target="_blank" href="https://gitlab.insrt.uk/revolt"
rel="noreferrer"> target="_blank"
<ButtonItem compact> rel="noreferrer">
<Gitlab size={20} /> <ButtonItem compact>
<Text id="app.settings.pages.source_code" /> <Gitlab size={20} />
<Text id="app.settings.pages.source_code" />
</ButtonItem>
</a>
<a
href="https://insrt.uk/donate"
target="_blank"
rel="noreferrer">
<ButtonItem className={styles.donate} compact>
<Coffee size={20} />
<Text id="app.settings.pages.donate.title" />
</ButtonItem>
</a>
<LineDivider />
<ButtonItem
onClick={() => operations.logout()}
className={styles.logOut}
compact>
<LogOut size={20} />
<Text id="app.settings.pages.logOut" />
</ButtonItem> </ButtonItem>
</a>, <div className={styles.version}>
<a <span className={styles.revision}>
href="https://insrt.uk/donate" <a
target="_blank" href={`${REPO_URL}/${GIT_REVISION}`}
rel="noreferrer"> target="_blank"
<ButtonItem className={styles.donate} compact> rel="noreferrer">
<Coffee size={20} /> {GIT_REVISION.substr(0, 7)}
<Text id="app.settings.pages.donate.title" /> </a>
</ButtonItem> {` `}
</a>, <a
<LineDivider />, href={
<ButtonItem GIT_BRANCH !== "DETACHED"
onClick={() => operations.logout()} ? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
className={styles.logOut} : undefined
compact> }
<LogOut size={20} /> target="_blank"
<Text id="app.settings.pages.logOut" /> rel="noreferrer">
</ButtonItem>, ({GIT_BRANCH})
<div className={styles.version}> </a>
<span className={styles.revision}> </span>
<a <span>
href={`${REPO_URL}/${GIT_REVISION}`} {GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
target="_blank" {APP_VERSION}
rel="noreferrer"> </span>
{GIT_REVISION.substr(0, 7)} {window.isNative && (
</a> <span>Native: {window.nativeVersion}</span>
{` `} )}
<a <span>
href={ API: {client.configuration?.revolt ?? "N/A"}
GIT_BRANCH !== "DETACHED" </span>
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}` <span>revolt.js: {LIBRARY_VERSION}</span>
: undefined </div>
} </>
target="_blank" }
rel="noreferrer">
({GIT_BRANCH})
</a>
</span>
<span>
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
{APP_VERSION}
</span>
{window.isNative && (
<span>Native: {window.nativeVersion}</span>
)}
<span>
API: {client.configuration?.revolt ?? "N/A"}
</span>
<span>revolt.js: {LIBRARY_VERSION}</span>
</div>,
]}
/> />
); );
} }

View file

@ -1,14 +1,13 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled, { css } from "styled-components"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads"; import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
@ -31,8 +30,6 @@ const Row = styled.div`
`; `;
export default observer(({ channel }: Props) => { export default observer(({ channel }: Props) => {
const client = useContext(AppContext);
const [name, setName] = useState(channel.name ?? undefined); const [name, setName] = useState(channel.name ?? undefined);
const [description, setDescription] = useState(channel.description ?? ""); const [description, setDescription] = useState(channel.description ?? "");
@ -44,7 +41,7 @@ export default observer(({ channel }: Props) => {
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
function save() { function save() {
const changes: any = {}; const changes: Record<string, string | undefined> = {};
if (name !== channel.name) changes.name = name; if (name !== channel.name) changes.name = name;
if (description !== channel.description) if (description !== channel.description)
changes.description = description; changes.description = description;

View file

@ -64,6 +64,7 @@ export default observer(({ channel }: Props) => {
return ( return (
<Checkbox <Checkbox
key={id}
checked={selected === id} checked={selected === id}
onChange={(selected) => selected && setSelected(id)}> onChange={(selected) => selected && setSelected(id)}>
{role.name} {role.name}

View file

@ -6,7 +6,7 @@ import {
Trash, Trash,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Profile } from "revolt-api/types/Users"; import { Profile } from "revolt-api/types/Users";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
@ -55,7 +55,7 @@ export const Account = observer(() => {
.user!.fetchProfile() .user!.fetchProfile()
.then((profile) => setProfile(profile ?? {})); .then((profile) => setProfile(profile ?? {}));
} }
}, [status]); }, [client, email, profile, status]);
return ( return (
<div className={styles.user}> <div className={styles.user}>
@ -95,12 +95,17 @@ export const Account = observer(() => {
<div> <div>
{( {(
[ [
["username", client.user!.username, <At size={24} />], [
["email", email, <Envelope size={24} />], "username",
["password", "•••••••••", <Key size={24} />], client.user!.username,
<At key="at" size={24} />,
],
["email", email, <Envelope key="envelope" size={24} />],
["password", "•••••••••", <Key key="key" size={24} />],
] as const ] as const
).map(([field, value, icon]) => ( ).map(([field, value, icon]) => (
<CategoryButton <CategoryButton
key={field}
icon={icon} icon={icon}
description={ description={
field === "email" ? ( field === "email" ? (
@ -152,13 +157,15 @@ export const Account = observer(() => {
</h3> </h3>
<h5> <h5>
{/*<Text id="app.settings.pages.account.2fa.description" />*/} {/*<Text id="app.settings.pages.account.2fa.description" />*/}
Two-factor authentication is currently work-in-progress, see {` `} Two-factor authentication is currently work-in-progress, see{" "}
{` `}
<a <a
href="https://gitlab.insrt.uk/insert/rauth/-/issues/2" href="https://gitlab.insrt.uk/insert/rauth/-/issues/2"
target="_blank" target="_blank"
rel="noreferrer"> rel="noreferrer">
tracking issue here tracking issue here
</a>. </a>
.
</h5> </h5>
<CategoryButton <CategoryButton
icon={<Lock size={24} color="var(--error)" />} icon={<Lock size={24} color="var(--error)" />}
@ -188,7 +195,7 @@ export const Account = observer(() => {
description={ description={
"Delete your account, including all of your data." "Delete your account, including all of your data."
} }
onClick={() => {}} hover
action="external"> action="external">
<Text id="app.settings.pages.account.manage.delete" /> <Text id="app.settings.pages.account.manage.delete" />
</CategoryButton> </CategoryButton>

View file

@ -1,6 +1,6 @@
import { Reset, Import } from "@styled-icons/boxicons-regular"; import { Reset, Import } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid"; import { Pencil } from "@styled-icons/boxicons-solid";
// @ts-ignore // @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color"; import pSBC from "shade-blend-color";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
@ -17,8 +17,10 @@ import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import { import {
DEFAULT_FONT, DEFAULT_FONT,
DEFAULT_MONO_FONT, DEFAULT_MONO_FONT,
Fonts,
FONTS, FONTS,
FONT_KEYS, FONT_KEYS,
MonospaceFonts,
MONOSPACE_FONTS, MONOSPACE_FONTS,
MONOSPACE_FONT_KEYS, MONOSPACE_FONT_KEYS,
Theme, Theme,
@ -30,6 +32,7 @@ import { useIntermediate } from "../../../context/intermediate/Intermediate";
import CollapsibleSection from "../../../components/common/CollapsibleSection"; import CollapsibleSection from "../../../components/common/CollapsibleSection";
import Tooltip from "../../../components/common/Tooltip"; import Tooltip from "../../../components/common/Tooltip";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import ColourSwatches from "../../../components/ui/ColourSwatches"; import ColourSwatches from "../../../components/ui/ColourSwatches";
import ComboBox from "../../../components/ui/ComboBox"; import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
@ -56,12 +59,12 @@ export function Component(props: Props) {
}); });
} }
function pushOverride(custom: Partial<Theme>) { const pushOverride = useCallback((custom: Partial<Theme>) => {
dispatch({ dispatch({
type: "SETTINGS_SET_THEME_OVERRIDE", type: "SETTINGS_SET_THEME_OVERRIDE",
custom, custom,
}); });
} }, []);
function setAccent(accent: string) { function setAccent(accent: string) {
setOverride({ setOverride({
@ -80,12 +83,14 @@ export function Component(props: Props) {
}); });
} }
const setOverride = useCallback(debounce(pushOverride, 200), []) as ( // eslint-disable-next-line react-hooks/exhaustive-deps
custom: Partial<Theme>, const setOverride = useCallback(
) => void; debounce(pushOverride as (...args: unknown[]) => void, 200),
[pushOverride],
) as (custom: Partial<Theme>) => void;
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? ""); const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
useEffect(() => setOverride({ css }), [css]); useEffect(() => setOverride({ css }), [setOverride, css]);
const selected = props.settings.theme?.preset ?? "dark"; const selected = props.settings.theme?.preset ?? "dark";
return ( return (
@ -169,15 +174,15 @@ export function Component(props: Props) {
<ComboBox <ComboBox
value={theme.font ?? DEFAULT_FONT} value={theme.font ?? DEFAULT_FONT}
onChange={(e) => onChange={(e) =>
pushOverride({ font: e.currentTarget.value as any }) pushOverride({ font: e.currentTarget.value as Fonts })
}> }>
{FONT_KEYS.map((key) => ( {FONT_KEYS.map((key) => (
<option value={key}> <option value={key} key={key}>
{FONTS[key as keyof typeof FONTS].name} {FONTS[key as keyof typeof FONTS].name}
</option> </option>
))} ))}
</ComboBox> </ComboBox>
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter. {/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.*/}
<p> <p>
<Checkbox <Checkbox
checked={props.settings.theme?.ligatures === true} checked={props.settings.theme?.ligatures === true}
@ -191,7 +196,7 @@ export function Component(props: Props) {
}> }>
<Text id="app.settings.pages.appearance.ligatures" /> <Text id="app.settings.pages.appearance.ligatures" />
</Checkbox> </Checkbox>
</p>*/} </p>
<h3> <h3>
<Text id="app.settings.pages.appearance.emoji_pack" /> <Text id="app.settings.pages.appearance.emoji_pack" />
@ -405,11 +410,12 @@ export function Component(props: Props) {
value={theme.monospaceFont ?? DEFAULT_MONO_FONT} value={theme.monospaceFont ?? DEFAULT_MONO_FONT}
onChange={(e) => onChange={(e) =>
pushOverride({ pushOverride({
monospaceFont: e.currentTarget.value as any, monospaceFont: e.currentTarget
.value as MonospaceFonts,
}) })
}> }>
{MONOSPACE_FONT_KEYS.map((key) => ( {MONOSPACE_FONT_KEYS.map((key) => (
<option value={key}> <option value={key} key={key}>
{ {
MONOSPACE_FONTS[ MONOSPACE_FONTS[
key as keyof typeof MONOSPACE_FONTS key as keyof typeof MONOSPACE_FONTS

View file

@ -23,6 +23,7 @@ export function Component(props: Props) {
</h3> </h3>
{AVAILABLE_EXPERIMENTS.map((key) => ( {AVAILABLE_EXPERIMENTS.map((key) => (
<Checkbox <Checkbox
key={key}
checked={(props.options?.enabled ?? []).indexOf(key) > -1} checked={(props.options?.enabled ?? []).indexOf(key) > -1}
onChange={(enabled) => onChange={(enabled) =>
dispatch({ dispatch({

View file

@ -70,7 +70,7 @@ export function Feedback() {
placeholder={ placeholder={
( (
<Text id="app.settings.pages.feedback.other" /> <Text id="app.settings.pages.feedback.other" />
) as any ) as unknown as string
} }
/> />
</Localizer> </Localizer>

View file

@ -1,15 +1,9 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { SyncOptions } from "../../../redux/reducers/sync";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox"; import Checkbox from "../../../components/ui/Checkbox";
interface Props { export function Native() {
options?: SyncOptions;
}
export function Native(props: Props) {
const [config, setConfig] = useState(window.native.getConfig()); const [config, setConfig] = useState(window.native.getConfig());
const [autoStart, setAutoStart] = useState<boolean | undefined>(); const [autoStart, setAutoStart] = useState<boolean | undefined>();
const fetchValue = () => window.native.getAutoStart().then(setAutoStart); const fetchValue = () => window.native.getAutoStart().then(setAutoStart);
@ -42,7 +36,6 @@ export function Native(props: Props) {
Start with computer Start with computer
</Checkbox> </Checkbox>
<Checkbox <Checkbox
checked={config.discordRPC} checked={config.discordRPC}
onChange={(discordRPC) => { onChange={(discordRPC) => {

View file

@ -127,6 +127,7 @@ export function Component({ options }: Props) {
</h3> </h3>
{SOUNDS_ARRAY.map((key) => ( {SOUNDS_ARRAY.map((key) => (
<Checkbox <Checkbox
key={key}
checked={!!enabledSounds[key]} checked={!!enabledSounds[key]}
onChange={(enabled) => onChange={(enabled) =>
dispatch({ dispatch({

View file

@ -1,8 +1,11 @@
import { Profile as ProfileI } from "revolt-api/types/Users";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { IntlContext, Text, translate } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useTranslation } from "../../../lib/i18n";
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile"; import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
import { FileUploader } from "../../../context/revoltjs/FileUploads"; import { FileUploader } from "../../../context/revoltjs/FileUploads";
@ -16,31 +19,27 @@ import AutoComplete, {
useAutoComplete, useAutoComplete,
} from "../../../components/common/AutoComplete"; } from "../../../components/common/AutoComplete";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import { Profile } from "revolt-api/types/Users";
export function Profile() { export function Profile() {
const { intl } = useContext(IntlContext);
const status = useContext(StatusContext); const status = useContext(StatusContext);
const translate = useTranslation();
const client = useClient(); const client = useClient();
const [profile, setProfile] = useState<undefined | Profile>( const [profile, setProfile] = useState<undefined | ProfileI>(undefined);
undefined,
);
// ! FIXME: temporary solution // ! FIXME: temporary solution
// ! we should just announce profile changes through WS // ! we should just announce profile changes through WS
function refreshProfile() { const refreshProfile = useCallback(() => {
client client
.user!.fetchProfile() .user!.fetchProfile()
.then((profile) => setProfile(profile ?? {})); .then((profile) => setProfile(profile ?? {}));
} }, [client.user, setProfile]);
useEffect(() => { useEffect(() => {
if (profile === undefined && status === ClientStatus.ONLINE) { if (profile === undefined && status === ClientStatus.ONLINE) {
refreshProfile(); refreshProfile();
} }
}, [status]); }, [profile, status, refreshProfile]);
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
function setContent(content?: string) { function setContent(content?: string) {
@ -69,7 +68,6 @@ export function Profile() {
user_id={client.user!._id} user_id={client.user!._id}
dummy={true} dummy={true}
dummyProfile={profile} dummyProfile={profile}
onClose={() => {}}
/> />
</div> </div>
<div className={styles.row}> <div className={styles.row}>
@ -85,9 +83,7 @@ export function Profile() {
behaviour="upload" behaviour="upload"
maxFileSize={4_000_000} maxFileSize={4_000_000}
onUpload={(avatar) => client.users.edit({ avatar })} onUpload={(avatar) => client.users.edit({ avatar })}
remove={() => remove={() => client.users.edit({ remove: "Avatar" })}
client.users.edit({ remove: "Avatar" })
}
defaultPreview={client.user!.generateAvatarURL( defaultPreview={client.user!.generateAvatarURL(
{ max_side: 256 }, { max_side: 256 },
true, true,
@ -152,8 +148,6 @@ export function Profile() {
? "fetching" ? "fetching"
: "placeholder" : "placeholder"
}`, }`,
"",
(intl as any).dictionary as Record<string, unknown>,
)} )}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}

View file

@ -50,7 +50,7 @@ export function Sessions() {
); );
setSessions(data); setSessions(data);
}); });
}, []); }, [client, setSessions, deviceId]);
if (typeof sessions === "undefined") { if (typeof sessions === "undefined") {
return ( return (
@ -123,6 +123,7 @@ export function Sessions() {
const systemIcon = getSystemIcon(session); const systemIcon = getSystemIcon(session);
return ( return (
<div <div
key={session.id}
className={styles.entry} className={styles.entry}
data-active={session.id === deviceId} data-active={session.id === deviceId}
data-deleting={ data-deleting={

View file

@ -26,6 +26,7 @@ export function Component(props: Props) {
] as [SyncKeys, string][] ] as [SyncKeys, string][]
).map(([key, title]) => ( ).map(([key, title]) => (
<Checkbox <Checkbox
key={key}
checked={ checked={
(props.options?.disabled ?? []).indexOf(key) === -1 (props.options?.disabled ?? []).indexOf(key) === -1
} }

View file

@ -5,9 +5,7 @@ import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import IconButton from "../../../components/ui/IconButton"; import IconButton from "../../../components/ui/IconButton";
@ -18,15 +16,14 @@ interface Props {
} }
export const Bans = observer(({ server }: Props) => { export const Bans = observer(({ server }: Props) => {
const client = useContext(AppContext);
const [deleting, setDelete] = useState<string[]>([]); const [deleting, setDelete] = useState<string[]>([]);
const [data, setData] = useState< const [data, setData] = useState<
Route<"GET", "/servers/id/bans">["response"] | undefined Route<"GET", "/servers/id/bans">["response"] | undefined
>(undefined); >(undefined);
useEffect(() => { useEffect(() => {
server.fetchBans().then(setData as any); server.fetchBans().then(setData);
}, []); }, [server, setData]);
return ( return (
<div className={styles.userList}> <div className={styles.userList}>
@ -43,10 +40,11 @@ export const Bans = observer(({ server }: Props) => {
</div> </div>
{typeof data === "undefined" && <Preloader type="ring" />} {typeof data === "undefined" && <Preloader type="ring" />}
{data?.bans.map((x) => { {data?.bans.map((x) => {
let user = data.users.find((y) => y._id === x._id.user); const user = data.users.find((y) => y._id === x._id.user);
return ( return (
<div <div
key={x._id.user}
className={styles.ban} className={styles.ban}
data-deleting={deleting.indexOf(x._id.user) > -1}> data-deleting={deleting.indexOf(x._id.user) > -1}>
<span> <span>

View file

@ -4,17 +4,12 @@ import { Category } from "revolt-api/types/Servers";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { useContext, useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import ChannelIcon from "../../../components/common/ChannelIcon"; import ChannelIcon from "../../../components/common/ChannelIcon";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import ComboBox from "../../../components/ui/ComboBox"; import ComboBox from "../../../components/ui/ComboBox";
import IconButton from "../../../components/ui/IconButton";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
import Preloader from "../../../components/ui/Preloader";
import Tip from "../../../components/ui/Tip"; import Tip from "../../../components/ui/Tip";
interface Props { interface Props {
@ -23,7 +18,6 @@ interface Props {
// ! FIXME: really bad code // ! FIXME: really bad code
export const Categories = observer(({ server }: Props) => { export const Categories = observer(({ server }: Props) => {
const client = useContext(AppContext);
const channels = server.channels.filter((x) => typeof x !== "undefined"); const channels = server.channels.filter((x) => typeof x !== "undefined");
const [cats, setCats] = useState<Category[]>(server.categories ?? []); const [cats, setCats] = useState<Category[]>(server.categories ?? []);
@ -95,6 +89,7 @@ export const Categories = observer(({ server }: Props) => {
{channels.map((channel) => { {channels.map((channel) => {
return ( return (
<div <div
key={channel!._id}
style={{ style={{
display: "flex", display: "flex",
gap: "12px", gap: "12px",
@ -131,7 +126,9 @@ export const Categories = observer(({ server }: Props) => {
}> }>
<option value="none">Uncategorised</option> <option value="none">Uncategorised</option>
{cats.map((x) => ( {cats.map((x) => (
<option value={x.id}>{x.title}</option> <option key={x.id} value={x.id}>
{x.title}
</option>
))} ))}
</ComboBox> </ComboBox>
</div> </div>

View file

@ -32,7 +32,7 @@ export const Invites = observer(({ server }: Props) => {
useEffect(() => { useEffect(() => {
server.fetchInvites().then(setInvites); server.fetchInvites().then(setInvites);
}, []); }, [server, setInvites]);
return ( return (
<div className={styles.userList}> <div className={styles.userList}>
@ -57,6 +57,7 @@ export const Invites = observer(({ server }: Props) => {
return ( return (
<div <div
key={invite._id}
className={styles.invite} className={styles.invite}
data-deleting={deleting.indexOf(invite._id) > -1}> data-deleting={deleting.indexOf(invite._id) > -1}>
<code>{invite._id}</code> <code>{invite._id}</code>

View file

@ -9,8 +9,6 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox"; import Checkbox from "../../../components/ui/Checkbox";
@ -29,7 +27,7 @@ export const Members = observer(({ server }: Props) => {
useEffect(() => { useEffect(() => {
server.fetchMembers().then(setData); server.fetchMembers().then(setData);
}, []); }, [server, setData]);
const [roles, setRoles] = useState<string[]>([]); const [roles, setRoles] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
@ -38,7 +36,7 @@ export const Members = observer(({ server }: Props) => {
data!.members.find((x) => x._id.user === selected)?.roles ?? [], data!.members.find((x) => x._id.user === selected)?.roles ?? [],
); );
} }
}, [selected]); }, [setRoles, selected, data]);
return ( return (
<div className={styles.userList}> <div className={styles.userList}>
@ -57,9 +55,10 @@ export const Members = observer(({ server }: Props) => {
}; };
}) })
.map(({ member, user }) => ( .map(({ member, user }) => (
<> // @ts-expect-error brokey
// eslint-disable-next-line react/jsx-no-undef
<Fragment key={member._id.user}>
<div <div
key={member._id.user}
className={styles.member} className={styles.member}
data-open={selected === member._id.user} data-open={selected === member._id.user}
onClick={() => onClick={() =>
@ -81,14 +80,15 @@ export const Members = observer(({ server }: Props) => {
</div> </div>
{selected === member._id.user && ( {selected === member._id.user && (
<div <div
key={"drop_" + member._id.user} key={`drop_${member._id.user}`}
className={styles.memberView}> className={styles.memberView}>
<Overline type="subtle">Roles</Overline> <Overline type="subtle">Roles</Overline>
{Object.keys(server.roles ?? {}).map( {Object.keys(server.roles ?? {}).map(
(key) => { (key) => {
let role = server.roles![key]; const role = server.roles![key];
return ( return (
<Checkbox <Checkbox
key={key}
checked={ checked={
roles.includes(key) ?? roles.includes(key) ??
false false
@ -134,7 +134,7 @@ export const Members = observer(({ server }: Props) => {
</Button> </Button>
</div> </div>
)} )}
</> </Fragment>
))} ))}
</div> </div>
); );

View file

@ -4,12 +4,11 @@ import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads"; import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext, useClient } from "../../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../../context/revoltjs/util"; import { getChannelName } from "../../../context/revoltjs/util";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
@ -21,8 +20,6 @@ interface Props {
} }
export const Overview = observer(({ server }: Props) => { export const Overview = observer(({ server }: Props) => {
const client = useClient();
const [name, setName] = useState(server.name); const [name, setName] = useState(server.name);
const [description, setDescription] = useState(server.description ?? ""); const [description, setDescription] = useState(server.description ?? "");
const [systemMessages, setSystemMessages] = useState( const [systemMessages, setSystemMessages] = useState(
@ -41,7 +38,7 @@ export const Overview = observer(({ server }: Props) => {
const [changed, setChanged] = useState(false); const [changed, setChanged] = useState(false);
function save() { function save() {
const changes: Record<string, any> = {}; const changes: Record<string, unknown> = {};
if (name !== server.name) changes.name = name; if (name !== server.name) changes.name = name;
if (description !== server.description) if (description !== server.description)
changes.description = description; changes.description = description;
@ -122,6 +119,7 @@ export const Overview = observer(({ server }: Props) => {
].map(([i18n, key]) => ( ].map(([i18n, key]) => (
// ! FIXME: temporary code just so we can expose the options // ! FIXME: temporary code just so we can expose the options
<p <p
key={key}
style={{ style={{
display: "flex", display: "flex",
gap: "8px", gap: "8px",
@ -156,7 +154,7 @@ export const Overview = observer(({ server }: Props) => {
{server.channels {server.channels
.filter((x) => typeof x !== "undefined") .filter((x) => typeof x !== "undefined")
.map((channel) => ( .map((channel) => (
<option value={channel!._id}> <option key={channel!._id} value={channel!._id}>
{getChannelName(channel!, true)} {getChannelName(channel!, true)}
</option> </option>
))} ))}

Some files were not shown because too many files have changed in this diff Show more