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-ssr
*.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",
"scripts": {
"dev": "vite",
"build": "rimraf build && vite build",
"preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"preact",
"plugin:@typescript-eslint/recommended"
],
"ignorePatterns": [
"build/"
],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off"
}
},
"dependencies": {
"preact": "^10.5.13",
"revolt-api": "0.5.1-alpha.10-patch.0"
},
"devDependencies": {
"@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/bree-serif": "^4.4.5",
"@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5",
"@fontsource/inter": "^4.4.5",
"@fontsource/lato": "^4.4.5",
"@fontsource/montserrat": "^4.4.5",
"@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",
"@fontsource/roboto-mono": "^4.4.5",
"@fontsource/source-code-pro": "^4.4.5",
"@fontsource/space-mono": "^4.4.5",
"@fontsource/ubuntu": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6",
"@preact/preset-vite": "^2.0.0",
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.34.0",
"@styled-icons/boxicons-regular": "^10.34.0",
"@styled-icons/boxicons-solid": "^10.37.0",
"@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "^4.2.5",
"@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.isequal": "^4.5.5",
"@types/markdown-it": "^12.0.2",
"@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5",
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2",
"@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.27.0",
"classnames": "^2.3.1",
"dayjs": "^1.10.6",
"detect-browser": "^5.2.0",
"eslint": "^7.28.0",
"eslint-config-preact": "^1.1.4",
"eventemitter3": "^4.0.7",
"highlight.js": "^11.0.1",
"idb": "^6.1.2",
"localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0",
"markdown-it": "^12.0.6",
"markdown-it-emoji": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact-context-menu": "^0.1.5",
"preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1",
"prismjs": "^1.23.0",
"react-device-detect": "^1.17.0",
"react-helmet": "^6.1.0",
"react-hook-form": "6.3.0",
"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.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": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b",
"vite-plugin-pwa": "^0.8.1",
"workbox-precaching": "^6.1.5"
}
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "rimraf build && vite build",
"preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
"preact",
"plugin:@typescript-eslint/recommended"
],
"ignorePatterns": [
"build/"
],
"rules": {
"radix": "off",
"no-spaced-func": "off",
"react/no-danger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^_"
}
],
"no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^_"
}
]
}
},
"dependencies": {
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b"
},
"devDependencies": {
"@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/bree-serif": "^4.4.5",
"@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5",
"@fontsource/inter": "^4.4.5",
"@fontsource/lato": "^4.4.5",
"@fontsource/montserrat": "^4.4.5",
"@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",
"@fontsource/roboto-mono": "^4.4.5",
"@fontsource/source-code-pro": "^4.4.5",
"@fontsource/space-mono": "^4.4.5",
"@fontsource/ubuntu": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6",
"@preact/preset-vite": "^2.0.0",
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.34.0",
"@styled-icons/boxicons-regular": "^10.34.0",
"@styled-icons/boxicons-solid": "^10.37.0",
"@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "^4.2.5",
"@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.isequal": "^4.5.5",
"@types/markdown-it": "^12.0.2",
"@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5",
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2",
"@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.27.0",
"classnames": "^2.3.1",
"dayjs": "^1.10.6",
"detect-browser": "^5.2.0",
"eslint": "^7.28.0",
"eslint-config-preact": "^1.1.4",
"eventemitter3": "^4.0.7",
"highlight.js": "^11.0.1",
"localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0",
"markdown-it": "^12.0.6",
"markdown-it-emoji": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.14",
"preact-context-menu": "^0.1.5",
"preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1",
"prismjs": "^1.23.0",
"react-device-detect": "^1.17.0",
"react-helmet": "^6.1.0",
"react-hook-form": "6.3.0",
"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 { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
@ -143,14 +142,16 @@ export function useAutoComplete(
) as User[];
break;
case "TextChannel":
const server = channel.server_id;
users = [...client.members.keys()]
.map((x) => JSON.parse(x))
.filter((x) => x.server === server)
.map((x) => client.users.get(x.user))
.filter(
(x) => typeof x !== "undefined",
) as User[];
{
const server = channel.server_id;
users = [...client.members.keys()]
.map((x) => JSON.parse(x))
.filter((x) => x.server === server)
.map((x) => client.users.get(x.user))
.filter(
(x) => typeof x !== "undefined",
) as User[];
}
break;
default:
return;
@ -304,7 +305,7 @@ export function useAutoComplete(
function onKeyUp(e: KeyboardEvent) {
if (e.currentTarget !== null) {
// @ts-expect-error
// @ts-expect-error Type mis-match.
onChange(e);
}
}
@ -391,6 +392,7 @@ export default function AutoComplete({
{state.type === "emoji" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
@ -422,6 +424,7 @@ export default function AutoComplete({
{state.type === "user" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
@ -446,6 +449,7 @@ export default function AutoComplete({
{state.type === "channel" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&

View file

@ -15,7 +15,11 @@ interface Props extends IconBaseProps<Channel> {
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);
@ -25,8 +29,6 @@ export default observer(
attachment,
isServerChannel: server,
animate,
children,
as,
...imgProps
} = props;
const iconURL = client.generateFileURL(

View file

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

View file

@ -22,23 +22,19 @@ const ServerText = styled.div`
background: var(--primary-background);
`;
const fallback = "/assets/group.png";
// const fallback = "/assets/group.png";
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 {
target,
attachment,
size,
animate,
server_name,
children,
as,
...imgProps
} = props;
const { target, attachment, size, animate, server_name, ...imgProps } =
props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
{ max_side: 256 },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -60,7 +60,7 @@ export default function TextFile({ attachment }: Props) {
setLoading(false);
});
}
}, [content, loading, status]);
}, [content, loading, status, attachment._id, attachment.size, url]);
return (
<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 styled from "styled-components";
@ -186,7 +187,9 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
<Container>
<Carousel>
{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 />}
<FileEntry
index={index}
@ -198,7 +201,7 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
: undefined
}
/>
</>
</Fragment>
))}
{state.type === "attached" && (
<EmptyEntry onClick={addFile}>

View file

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

View file

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

View file

@ -52,20 +52,23 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
`;
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 {
target,
attachment,
size,
voice,
status,
animate,
mask,
hover,
children,
as,
...svgProps
} = props;
const iconURL =

View file

@ -31,10 +31,10 @@ export const Username = observer(
}
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) {
for (let role of member.roles) {
let c = srv.roles[role].colour;
for (const role of member.roles) {
const c = srv.roles[role].colour;
if (c) {
color = c;
continue;

View file

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

View file

@ -1,19 +1,20 @@
/* eslint-disable react-hooks/rules-of-hooks */
import MarkdownKatex from "@traptitech/markdown-it-katex";
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
import "katex/dist/katex.min.css";
import MarkdownIt from "markdown-it";
// @ts-ignore
// @ts-expect-error No typings.
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
// @ts-ignore
// @ts-expect-error No typings.
import MarkdownSub from "markdown-it-sub";
// @ts-ignore
// @ts-expect-error No typings.
import MarkdownSup from "markdown-it-sup";
import Prism from "prismjs";
import "prismjs/themes/prism-tomorrow.css";
import { RE_MENTIONS } from "revolt.js";
import styles from "./Markdown.module.scss";
import { useCallback, useContext, useRef } from "preact/hooks";
import { useCallback, useContext } from "preact/hooks";
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 don't care if the mention changes.
const newContent = content
.replace(RE_MENTIONS, (sub: string, ...args: any[]) => {
const id = args[0],
.replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
const id = args[0] as string,
user = client.users.get(id);
if (user) {
@ -105,8 +106,8 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
return sub;
})
.replace(RE_CHANNELS, (sub: string, ...args: any[]) => {
const id = args[0],
.replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
const id = args[0] as string,
channel = client.channels.get(id);
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 Tooltip from "../common/Tooltip";
import UpdateIndicator from "../common/UpdateIndicator";
const TitlebarBase = styled.div`
@ -16,11 +15,12 @@ const TitlebarBase = styled.div`
margin-top: 10px;
height: 100%;
}
.quick {
color: var(--secondary-foreground);
> div, > div > div {
> div,
> div > div {
width: var(--titlebar-height) !important;
}
@ -99,7 +99,9 @@ export function Titlebar() {
stroke-width="1"
/>
</svg>
{window.native.getConfig().build === "dev" && <Wrench size="12.5"/>}
{window.native.getConfig().build === "dev" && (
<Wrench size="12.5" />
)}
</div>
{/*<div class="actions quick">
<Tooltip
@ -121,13 +123,50 @@ export function Titlebar() {
<UpdateIndicator style="titlebar" />
<div class="actions">
<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 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 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>
</TitlebarBase>

View file

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

View file

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

View file

@ -7,11 +7,11 @@ import { useEffect } from "preact/hooks";
import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useClient } from "../../../context/revoltjs/RevoltClient";
@ -38,9 +38,9 @@ const ServerBase = styled.div`
overflow: hidden;
${isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
css`
padding-bottom: 50px;
`}
`;
const ServerList = styled.div`
@ -73,7 +73,7 @@ const ServerSidebar = observer((props: Props) => {
parent: server_id!,
child: channel_id!,
});
}, [channel_id]);
}, [channel_id, server_id]);
const uncategorised = new Set(server.channel_ids);
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 { useLayoutEffect } from "preact/hooks";
@ -6,16 +6,12 @@ import { useLayoutEffect } from "preact/hooks";
import { dispatch } from "../../../redux";
import { Unreads } from "../../../redux/reducers/unreads";
import { useClient } from "../../../context/revoltjs/RevoltClient";
type UnreadProps = {
channel: Channel;
unreads: Unreads;
};
export function useUnreads({ channel, unreads }: UnreadProps) {
const client = useClient();
useLayoutEffect(() => {
function checkUnread(target: Channel) {
if (!target) return;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import update from "dayjs/plugin/updateLocale";
import defaultsDeep from "lodash.defaultsdeep";
import { IntlProvider } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { useCallback, useEffect, useState } from "preact/hooks";
import { connectState } from "../redux/connector";
@ -165,12 +165,14 @@ export interface Dictionary {
}
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.
const lang = Languages[locale] ?? Languages.en;
function transformLanguage(source: { [key: string]: any }) {
function transformLanguage(source: Dictionary) {
// Fallback untranslated strings to English (UK)
const obj = defaultsDeep(source, definition);
@ -216,43 +218,46 @@ function Locale({ children, locale }: Props) {
return obj;
}
function loadLanguage(locale: string) {
if (locale === "en") {
// If English, make sure to restore everything to defaults.
// Use what we already have.
const defn = transformLanguage(definition);
setDefinition(defn);
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.
const loadLanguage = useCallback(
(locale: string) => {
if (locale === "en") {
// If English, make sure to restore everything to defaults.
// Use what we already have.
const defn = transformLanguage(definition as Dictionary);
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(() => {
// Apply RTL language format.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,6 @@ import Preloader from "../../../components/ui/Preloader";
import Markdown from "../../../components/markdown/Markdown";
import {
AppContext,
ClientStatus,
StatusContext,
useClient,
@ -30,7 +29,7 @@ import { useIntermediate } from "../Intermediate";
interface Props {
user_id: string;
dummy?: boolean;
onClose: () => void;
onClose?: () => void;
dummyProfile?: Profile;
}
@ -60,7 +59,7 @@ export const UserProfile = observer(
const user = client.users.get(user_id);
if (!user) {
useEffect(onClose, []);
if (onClose) useEffect(onClose, []);
return null;
}
@ -76,7 +75,7 @@ export const UserProfile = observer(
if (!user_id) return;
if (typeof profile !== "undefined") setProfile(undefined);
if (typeof mutual !== "undefined") setMutual(undefined);
}, [user_id]);
}, [user_id, mutual, profile]);
if (dummy) {
useLayoutEffect(() => {
@ -93,7 +92,7 @@ export const UserProfile = observer(
setMutual(null);
user.fetchMutual().then(setMutual);
}
}, [mutual, status]);
}, [mutual, status, dummy, user]);
useEffect(() => {
if (dummy) return;
@ -104,12 +103,10 @@ export const UserProfile = observer(
setProfile(null);
if (user.permission & UserPermission.ViewProfile) {
user.fetchProfile()
.then(setProfile)
.catch(() => {});
user.fetchProfile().then(setProfile);
}
}
}, [profile, status]);
}, [profile, status, dummy, user]);
const backgroundURL =
profile &&
@ -157,7 +154,7 @@ export const UserProfile = observer(
}>
<IconButton
onClick={() => {
onClose();
onClose?.();
history.push(`/open/${user_id}`);
}}>
<Envelope size={30} />
@ -168,7 +165,7 @@ export const UserProfile = observer(
{user.relationship === RelationshipStatus.User && (
<IconButton
onClick={() => {
onClose();
onClose?.();
if (dummy) return;
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 Axios, { AxiosRequestConfig } from "axios";
@ -147,6 +147,7 @@ export function FileUploader(props: Props) {
}
if (props.behaviour === "multi" && props.append) {
// eslint-disable-next-line
useEffect(() => {
// File pasting.
function paste(e: ClipboardEvent) {
@ -210,7 +211,7 @@ export function FileUploader(props: Props) {
document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop);
};
}, [props.append]);
}, [openScreen, props, props.append]);
}
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 { Presence, RelationshipStatus } from "revolt-api/types/Users";
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 { decodeTime } from "ulid";
import { useContext, useEffect } from "preact/hooks";
import { useCallback, useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n";
@ -52,191 +51,206 @@ function Notifier({ options, notifs }: Props) {
const history = useHistory();
const playSound = useContext(SoundContext);
async function message(msg: Message) {
if (msg.author_id === client.user!._id) return;
if (msg.channel_id === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Presence.Busy) return;
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
const message = useCallback(
async (msg: Message) => {
if (msg.author_id === client.user!._id) return;
if (msg.channel_id === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Presence.Busy) return;
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
const notifState = getNotificationState(notifs, msg.channel!);
if (!shouldNotify(notifState, msg, client.user!._id)) return;
const notifState = getNotificationState(notifs, msg.channel!);
if (!shouldNotify(notifState, msg, client.user!._id)) return;
playSound("message");
if (!showNotification) return;
playSound("message");
if (!showNotification) return;
let title;
switch (msg.channel?.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${msg.author?.username}`;
break;
case "Group":
if (msg.author?._id === SYSTEM_USER_ID) {
title = msg.channel.name;
} else {
title = `@${msg.author?.username} - ${msg.channel.name}`;
let title;
switch (msg.channel?.channel_type) {
case "SavedMessages":
return;
case "DirectMessage":
title = `@${msg.author?.username}`;
break;
case "Group":
if (msg.author?._id === SYSTEM_USER_ID) {
title = msg.channel.name;
} else {
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;
if (typeof msg.content === "string") {
body = client.markdownToText(msg.content);
icon = msg.author?.generateAvatarURL({ max_side: 256 });
} else {
const users = client.users;
switch (msg.content.type) {
case "user_added":
case "user_remove":
{
let user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: user?.username,
other_user: users.get(msg.content.by)?.username,
},
);
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}`,
let body, icon;
if (typeof msg.content === "string") {
body = client.markdownToText(msg.content);
icon = msg.author?.generateAvatarURL({ max_side: 256 });
} else {
const users = client.users;
switch (msg.content.type) {
case "user_added":
case "user_remove":
{
const user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: user?.username,
other_user: users.get(msg.content.by)
?.username,
},
);
} else {
history.push(`/channel/${id}`);
icon = user?.generateAvatarURL({
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;
notif.addEventListener(
"close",
() => delete notifications[msg.channel_id],
);
}
}
async function relationship(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,
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 {
history.push(`/channel/${id}`);
}
}
}
});
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(),
});
notifications[msg.channel_id] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel_id],
);
}
},
[
history,
showNotification,
translate,
channel_id,
client,
notifs,
playSound,
],
);
notif?.addEventListener("click", () => {
history.push(`/friends`);
});
}
const relationship = useCallback(
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(() => {
client.addListener("message", message);
@ -246,7 +260,16 @@ function Notifier({ options, notifs }: Props) {
client.removeListener("message", message);
client.removeListener("user/relationship", relationship);
};
}, [client, playSound, guild_id, channel_id, showNotification, notifs]);
}, [
client,
playSound,
guild_id,
channel_id,
showNotification,
notifs,
message,
relationship,
]);
useEffect(() => {
function visChange() {

View file

@ -1,5 +1,4 @@
import { openDB } from "idb";
import { useHistory } from "react-router-dom";
/* eslint-disable react-hooks/rules-of-hooks */
import { Client } from "revolt.js";
import { Route } from "revolt.js/dist/api/routes";
@ -58,29 +57,6 @@ function Context({ auth, children }: Props) {
useEffect(() => {
(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({
autoReconnect: false,
apiURL: import.meta.env.VITE_API_URL,
@ -146,11 +122,11 @@ function Context({ auth, children }: Props) {
ready: () =>
operations.loggedIn() && typeof client.user !== "undefined",
};
}, [client, auth.active]);
}, [client, auth.active, openScreen]);
useEffect(
() => registerEvents({ operations }, setStatus, client),
[client],
[client, operations],
);
useEffect(() => {
@ -203,6 +179,7 @@ function Context({ auth, children }: Props) {
setStatus(ClientStatus.READY);
}
})();
// eslint-disable-next-line
}, []);
if (status === ClientStatus.LOADING) {

View file

@ -37,7 +37,7 @@ function StateMonitor(props: Props) {
client.addListener("message", add);
return () => client.removeListener("message", add);
}, [props.messages]);
}, [client, props.messages]);
return null;
}

View file

@ -5,7 +5,7 @@ import isEqual from "lodash.isequal";
import { UserSettings } from "revolt-api/types/Sync";
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 { connectState } from "../../redux/connector";
@ -28,7 +28,7 @@ type Props = {
notifications: Notifications;
};
const lastValues: { [key in SyncKeys]?: any } = {};
const lastValues: { [key in SyncKeys]?: unknown } = {};
export function mapSync(
packet: UserSettings,
@ -78,31 +78,38 @@ function SyncManager(props: Props) {
.syncFetchUnreads()
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
}
}, [status]);
}, [client, props.sync?.disabled, status]);
function syncChange(key: SyncKeys, data: any) {
const timestamp = +new Date();
dispatch({
type: "SYNC_SET_REVISION",
key,
timestamp,
});
const syncChange = useCallback(
(key: SyncKeys, data: unknown) => {
const timestamp = +new Date();
dispatch({
type: "SYNC_SET_REVISION",
key,
timestamp,
});
client.syncSetSettings(
{
[key]: data,
},
timestamp,
);
}
client.syncSetSettings(
{
[key]: data as string,
},
timestamp,
);
},
[client],
);
const disabled = props.sync.disabled ?? [];
const disabled = useMemo(
() => props.sync.disabled ?? [],
[props.sync.disabled],
);
for (const [key, object] of [
["appearance", props.settings.appearance],
["theme", props.settings.theme],
["locale", props.locale],
["notifications", props.notifications],
] as [SyncKeys, any][]) {
] as [SyncKeys, unknown][]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (disabled.indexOf(key) === -1) {
if (typeof lastValues[key] !== "undefined") {
@ -113,7 +120,7 @@ function SyncManager(props: Props) {
}
lastValues[key] = object;
}, [disabled, object]);
}, [key, syncChange, disabled, object]);
}
useEffect(() => {
@ -131,7 +138,7 @@ function SyncManager(props: Props) {
client.addListener("packet", onPacket);
return () => client.removeListener("packet", onPacket);
}, [disabled, props.sync]);
}, [client, disabled, props.sync]);
return null;
}

View file

@ -8,7 +8,7 @@ import { dispatch } from "../../redux";
import { ClientOperations, ClientStatus } from "./RevoltClient";
export var preventReconnect = false;
export let preventReconnect = false;
let preventUntil = 0;
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> = {
connecting: () =>
operations.ready() && setStatus(ClientStatus.CONNECTING),
@ -74,7 +75,7 @@ export function registerEvents(
if (import.meta.env.DEV) {
listeners = new Proxy(listeners, {
get:
(target, listener, receiver) =>
(target, listener) =>
(...args: unknown[]) => {
console.debug(`Calling ${listener.toString()} with`, args);
Reflect.get(target, listener)(...args);
@ -87,10 +88,6 @@ export function registerEvents(
client.addListener(listener, listeners[listener]);
}
function logMutation(target: string, key: string) {
console.log("(o) Object mutated", target, "\nChanged:", key);
}
const online = () => {
if (operations.ready()) {
setStatus(ClientStatus.RECONNECTING);

View file

@ -1,10 +1,10 @@
import { Client } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Text } from "preact-i18n";
import { Children } from "../../types/Preact";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function takeError(error: any): string {
const type = error?.response?.data?.type;
const id = type;

4
src/globals.d.ts vendored
View file

@ -18,10 +18,12 @@ declare interface Window {
relaunch();
getConfig(): NativeConfig;
set(key: keyof NativeConfig, value: any);
set(key: keyof NativeConfig, value: unknown);
getAutoStart(): Promise<boolean>;
enableAutoStart(): 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 {
ContextMenu,
ContextMenuWithData,
MenuItem,
openContextMenu,
@ -42,12 +41,11 @@ import {
} from "../redux/reducers/notifications";
import { QueuedMessage } from "../redux/reducers/queue";
import { useIntermediate } from "../context/intermediate/Intermediate";
import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
import {
AppContext,
ClientStatus,
StatusContext,
useClient,
} from "../context/revoltjs/RevoltClient";
import { takeError } from "../context/revoltjs/util";
@ -179,7 +177,7 @@ function ContextMenus(props: Props) {
case "retry_message":
{
const nonce = data.message.id;
const fail = (error: any) =>
const fail = (error: string) =>
dispatch({
type: "QUEUE_FAIL",
nonce,
@ -369,7 +367,8 @@ function ContextMenus(props: Props) {
case "clear_status":
{
const { text, ...status } = client.user?.status ?? {};
const { text: _text, ...status } =
client.user?.status ?? {};
await client.users.edit({ status });
}
break;
@ -382,12 +381,12 @@ function ContextMenus(props: Props) {
case "delete_message":
case "create_channel":
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({
id: "special_prompt",
type: data.action,
target: data.target as any,
});
target: data.target,
} as unknown as Screen);
break;
case "ban_member":
@ -596,8 +595,11 @@ function ContextMenus(props: Props) {
}
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
generateAction({ action: actions[i] as any, user });
// Typescript can't determine that user the actions are linked together correctly
generateAction({
action: actions[i],
user,
} as unknown as Action);
}
}
@ -968,6 +970,7 @@ function ContextMenus(props: Props) {
const elements: Children[] = [
<MenuItem
key="notif"
data={{
action: "set_notification_state",
key: channel._id,
@ -987,6 +990,7 @@ function ContextMenus(props: Props) {
function generate(key: string, icon: Children) {
elements.push(
<MenuItem
key={key}
data={{
action: "set_notification_state",
key: channel._id,

View file

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useState } from "preact/hooks";
const counts: { [key: string]: number } = {};

View file

@ -10,7 +10,7 @@ import { isTouchscreenDevice } from "./isTouchscreenDevice";
type TextAreaAutoSizeProps = Omit<
JSX.HTMLAttributes<HTMLTextAreaElement>,
"style" | "value" | "onChange"
"style" | "value" | "onChange" | "children" | "as"
> &
TextAreaProps & {
forceFocus?: boolean;
@ -63,8 +63,6 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
lineHeight,
hideBorder,
forceFocus,
children,
as,
onChange,
...textAreaProps
} = props;
@ -81,7 +79,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
useEffect(() => {
if (isTouchscreenDevice) return;
autoFocus && ref.current && ref.current.focus();
}, [value]);
}, [value, autoFocus]);
const inputSelected = () =>
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
@ -114,7 +112,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]);
}, [ref, autoFocus, forceFocus, value]);
useEffect(() => {
if (!ref.current) return;
@ -124,8 +122,12 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
}
}
return internalSubscribe("TextArea", "focus", focus);
}, [ref]);
return internalSubscribe(
"TextArea",
"focus",
focus as (...args: unknown[]) => void,
);
}, [props.id, ref]);
return (
<Container>

View file

@ -1,7 +1,7 @@
export function urlBase64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/-/g, "+")
.replace(/_/g, "/");
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.
let timer: NodeJS.Timeout;
// This function is given to React.
return (...args: any[]) => {
return (...args: unknown[]) => {
// Get rid of the old timer.
clearTimeout(timer);
// Set a new timer.

View file

@ -5,13 +5,13 @@ export const InternalEvent = new EventEmitter();
export function internalSubscribe(
ns: string,
event: string,
fn: (...args: any[]) => void,
fn: (...args: unknown[]) => void,
) {
InternalEvent.addListener(`${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);
}

View file

@ -47,7 +47,7 @@ export function TextReact({ id, fields }: Props) {
const path = id.split(".");
let entry = intl.dictionary[path.shift()!];
for (const key of path) {
// @ts-expect-error
// @ts-expect-error TODO: lazy
entry = entry[key];
}
@ -56,8 +56,12 @@ export function TextReact({ id, fields }: Props) {
export function useTranslation() {
const { intl } = useContext(IntlContext) as unknown as IntlType;
return (id: string, fields?: Object, plural?: number, fallback?: string) =>
translate(id, "", intl.dictionary, fields, plural, fallback);
return (
id: string,
fields?: Record<string, string | undefined>,
plural?: number,
fallback?: string,
) => translate(id, "", intl.dictionary, fields, plural, fallback);
}
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 { Client } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
@ -123,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 {
window
.getComputedStyle(child)
.marginTop.slice(0, -2),
10,
);
}
}
@ -167,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 {
window
.getComputedStyle(child)
.marginTop.slice(0, -2),
10,
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ import {
User,
Megaphone,
} 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 styles from "./Settings.module.scss";
@ -120,103 +120,107 @@ export default function Settings() {
title: <Text id="app.settings.pages.feedback.title" />,
},
]}
children={[
<Route path="/settings/profile">
<Profile />
</Route>,
<Route path="/settings/sessions">
<RequiresOnline>
<Sessions />
</RequiresOnline>
</Route>,
<Route path="/settings/appearance">
<Appearance />
</Route>,
<Route path="/settings/notifications">
<Notifications />
</Route>,
<Route path="/settings/language">
<Languages />
</Route>,
<Route path="/settings/sync">
<Sync />
</Route>,
<Route path="/settings/native">
<Native />
</Route>,
<Route path="/settings/experiments">
<ExperimentsPage />
</Route>,
<Route path="/settings/feedback">
<Feedback />
</Route>,
<Route path="/">
<Account />
</Route>,
]}
children={
<Switch>
<Route path="/settings/profile">
<Profile />
</Route>
<Route path="/settings/sessions">
<RequiresOnline>
<Sessions />
</RequiresOnline>
</Route>
<Route path="/settings/appearance">
<Appearance />
</Route>
<Route path="/settings/notifications">
<Notifications />
</Route>
<Route path="/settings/language">
<Languages />
</Route>
<Route path="/settings/sync">
<Sync />
</Route>
<Route path="/settings/native">
<Native />
</Route>
<Route path="/settings/experiments">
<ExperimentsPage />
</Route>
<Route path="/settings/feedback">
<Feedback />
</Route>
<Route path="/" exact>
<Account />
</Route>
</Switch>
}
defaultPage="account"
switchPage={switchPage}
category="pages"
custom={[
<a
href="https://gitlab.insrt.uk/revolt"
target="_blank"
rel="noreferrer">
<ButtonItem compact>
<Gitlab size={20} />
<Text id="app.settings.pages.source_code" />
custom={
<>
<a
href="https://gitlab.insrt.uk/revolt"
target="_blank"
rel="noreferrer">
<ButtonItem compact>
<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>
</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>,
<div className={styles.version}>
<span className={styles.revision}>
<a
href={`${REPO_URL}/${GIT_REVISION}`}
target="_blank"
rel="noreferrer">
{GIT_REVISION.substr(0, 7)}
</a>
{` `}
<a
href={
GIT_BRANCH !== "DETACHED"
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
: undefined
}
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>,
]}
<div className={styles.version}>
<span className={styles.revision}>
<a
href={`${REPO_URL}/${GIT_REVISION}`}
target="_blank"
rel="noreferrer">
{GIT_REVISION.substr(0, 7)}
</a>
{` `}
<a
href={
GIT_BRANCH !== "DETACHED"
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
: undefined
}
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 { Channel } from "revolt.js/dist/maps/Channels";
import styled, { css } from "styled-components";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
@ -31,8 +30,6 @@ const Row = styled.div`
`;
export default observer(({ channel }: Props) => {
const client = useContext(AppContext);
const [name, setName] = useState(channel.name ?? undefined);
const [description, setDescription] = useState(channel.description ?? "");
@ -44,7 +41,7 @@ export default observer(({ channel }: Props) => {
const [changed, setChanged] = useState(false);
function save() {
const changes: any = {};
const changes: Record<string, string | undefined> = {};
if (name !== channel.name) changes.name = name;
if (description !== channel.description)
changes.description = description;

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { Reset, Import } from "@styled-icons/boxicons-regular";
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 styles from "./Panes.module.scss";
@ -17,8 +17,10 @@ import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import {
DEFAULT_FONT,
DEFAULT_MONO_FONT,
Fonts,
FONTS,
FONT_KEYS,
MonospaceFonts,
MONOSPACE_FONTS,
MONOSPACE_FONT_KEYS,
Theme,
@ -30,6 +32,7 @@ import { useIntermediate } from "../../../context/intermediate/Intermediate";
import CollapsibleSection from "../../../components/common/CollapsibleSection";
import Tooltip from "../../../components/common/Tooltip";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import ColourSwatches from "../../../components/ui/ColourSwatches";
import ComboBox from "../../../components/ui/ComboBox";
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({
type: "SETTINGS_SET_THEME_OVERRIDE",
custom,
});
}
}, []);
function setAccent(accent: string) {
setOverride({
@ -80,12 +83,14 @@ export function Component(props: Props) {
});
}
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
custom: Partial<Theme>,
) => void;
// eslint-disable-next-line react-hooks/exhaustive-deps
const setOverride = useCallback(
debounce(pushOverride as (...args: unknown[]) => void, 200),
[pushOverride],
) as (custom: Partial<Theme>) => void;
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";
return (
@ -169,15 +174,15 @@ export function Component(props: Props) {
<ComboBox
value={theme.font ?? DEFAULT_FONT}
onChange={(e) =>
pushOverride({ font: e.currentTarget.value as any })
pushOverride({ font: e.currentTarget.value as Fonts })
}>
{FONT_KEYS.map((key) => (
<option value={key}>
<option value={key} key={key}>
{FONTS[key as keyof typeof FONTS].name}
</option>
))}
</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>
<Checkbox
checked={props.settings.theme?.ligatures === true}
@ -191,7 +196,7 @@ export function Component(props: Props) {
}>
<Text id="app.settings.pages.appearance.ligatures" />
</Checkbox>
</p>*/}
</p>
<h3>
<Text id="app.settings.pages.appearance.emoji_pack" />
@ -405,11 +410,12 @@ export function Component(props: Props) {
value={theme.monospaceFont ?? DEFAULT_MONO_FONT}
onChange={(e) =>
pushOverride({
monospaceFont: e.currentTarget.value as any,
monospaceFont: e.currentTarget
.value as MonospaceFonts,
})
}>
{MONOSPACE_FONT_KEYS.map((key) => (
<option value={key}>
<option value={key} key={key}>
{
MONOSPACE_FONTS[
key as keyof typeof MONOSPACE_FONTS

View file

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

View file

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

View file

@ -1,15 +1,9 @@
import { useEffect, useState } from "preact/hooks";
import { SyncOptions } from "../../../redux/reducers/sync";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
interface Props {
options?: SyncOptions;
}
export function Native(props: Props) {
export function Native() {
const [config, setConfig] = useState(window.native.getConfig());
const [autoStart, setAutoStart] = useState<boolean | undefined>();
const fetchValue = () => window.native.getAutoStart().then(setAutoStart);
@ -41,8 +35,7 @@ export function Native(props: Props) {
description="Launch Revolt when you log into your computer.">
Start with computer
</Checkbox>
<Checkbox
checked={config.discordRPC}
onChange={(discordRPC) => {
@ -181,4 +174,4 @@ export function Native(props: Props) {
)}
</div>
);
}
}

View file

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

View file

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

View file

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

View file

@ -26,6 +26,7 @@ export function Component(props: Props) {
] as [SyncKeys, string][]
).map(([key, title]) => (
<Checkbox
key={key}
checked={
(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 { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useEffect, useState } from "preact/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import IconButton from "../../../components/ui/IconButton";
@ -18,15 +16,14 @@ interface Props {
}
export const Bans = observer(({ server }: Props) => {
const client = useContext(AppContext);
const [deleting, setDelete] = useState<string[]>([]);
const [data, setData] = useState<
Route<"GET", "/servers/id/bans">["response"] | undefined
>(undefined);
useEffect(() => {
server.fetchBans().then(setData as any);
}, []);
server.fetchBans().then(setData);
}, [server, setData]);
return (
<div className={styles.userList}>
@ -43,10 +40,11 @@ export const Bans = observer(({ server }: Props) => {
</div>
{typeof data === "undefined" && <Preloader type="ring" />}
{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 (
<div
key={x._id.user}
className={styles.ban}
data-deleting={deleting.indexOf(x._id.user) > -1}>
<span>

View file

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

View file

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

View file

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

View file

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

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