mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-23 22:22:07 -05:00
Make the linter happy.
This commit is contained in:
parent
5930415c05
commit
55e00bf93f
106 changed files with 1083 additions and 5554 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ node_modules
|
|||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.log
|
||||
|
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"recommendations": ["esbenp.prettier-vscode"]
|
||||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
|
|
240
package.json
240
package.json
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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!}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 })}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Embed } from "revolt-api/types/January";
|
||||
|
||||
import styles from "./Embed.module.scss";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}` : ""
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { useRenderState } from "../../../lib/renderer/Singleton";
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -64,7 +64,7 @@ export default function Tip(
|
|||
{!hideSeparator && <Separator />}
|
||||
<TipBase {...tipProps}>
|
||||
<InfoCircle size={20} />
|
||||
<span>{props.children}</span>
|
||||
<span>{children}</span>
|
||||
</TipBase>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -42,6 +42,7 @@ export function UserPicker(props: Props) {
|
|||
)
|
||||
.map((x) => (
|
||||
<UserCheckbox
|
||||
key={x._id}
|
||||
user={x}
|
||||
checked={selected.includes(x._id)}
|
||||
onChange={(v) => {
|
||||
|
|
|
@ -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`);
|
||||
}}>
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -37,7 +37,7 @@ function StateMonitor(props: Props) {
|
|||
|
||||
client.addListener("message", add);
|
||||
return () => client.removeListener("message", add);
|
||||
}, [props.messages]);
|
||||
}, [client, props.messages]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
4
src/globals.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
const counts: { [key: string]: number } = {};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
4
src/lib/js.ts
Normal 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 */
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export const stopPropagation = (
|
||||
ev: JSX.TargetedMouseEvent<HTMLElement>,
|
||||
_consume?: any,
|
||||
// eslint-disable-next-line
|
||||
_consume?: unknown,
|
||||
) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -114,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
|
|||
|
||||
this.signaling.on(
|
||||
"error",
|
||||
(error) => {
|
||||
() => {
|
||||
this.emit("error", new Error("Signaling error"));
|
||||
},
|
||||
this,
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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" />;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -64,6 +64,7 @@ export default observer(({ channel }: Props) => {
|
|||
|
||||
return (
|
||||
<Checkbox
|
||||
key={id}
|
||||
checked={selected === id}
|
||||
onChange={(selected) => selected && setSelected(id)}>
|
||||
{role.name}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -70,7 +70,7 @@ export function Feedback() {
|
|||
placeholder={
|
||||
(
|
||||
<Text id="app.settings.pages.feedback.other" />
|
||||
) as any
|
||||
) as unknown as string
|
||||
}
|
||||
/>
|
||||
</Localizer>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,6 +127,7 @@ export function Component({ options }: Props) {
|
|||
</h3>
|
||||
{SOUNDS_ARRAY.map((key) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
checked={!!enabledSounds[key]}
|
||||
onChange={(enabled) =>
|
||||
dispatch({
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue