mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-21 14:40:58 -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
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.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",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "rimraf build && vite build",
|
"build": "rimraf build && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||||
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
|
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"extends": [
|
"extends": [
|
||||||
"preact",
|
"preact",
|
||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended"
|
||||||
],
|
],
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"build/"
|
"build/"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
"radix": "off",
|
||||||
}
|
"no-spaced-func": "off",
|
||||||
},
|
"react/no-danger": "off",
|
||||||
"dependencies": {
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
"preact": "^10.5.13",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
"revolt-api": "0.5.1-alpha.10-patch.0"
|
"@typescript-eslint/no-unused-vars": [
|
||||||
},
|
"warn",
|
||||||
"devDependencies": {
|
{
|
||||||
"@fontsource/atkinson-hyperlegible": "^4.4.5",
|
"varsIgnorePattern": "^_"
|
||||||
"@fontsource/bree-serif": "^4.4.5",
|
}
|
||||||
"@fontsource/comic-neue": "^4.4.5",
|
],
|
||||||
"@fontsource/fira-code": "^4.4.5",
|
"no-unused-vars": [
|
||||||
"@fontsource/inter": "^4.4.5",
|
"warn",
|
||||||
"@fontsource/lato": "^4.4.5",
|
{
|
||||||
"@fontsource/montserrat": "^4.4.5",
|
"varsIgnorePattern": "^_"
|
||||||
"@fontsource/noto-sans": "^4.4.5",
|
}
|
||||||
"@fontsource/open-sans": "^4.4.5",
|
]
|
||||||
"@fontsource/poppins": "^4.4.5",
|
}
|
||||||
"@fontsource/raleway": "^4.4.5",
|
},
|
||||||
"@fontsource/roboto": "^4.4.5",
|
"dependencies": {
|
||||||
"@fontsource/roboto-mono": "^4.4.5",
|
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b"
|
||||||
"@fontsource/source-code-pro": "^4.4.5",
|
},
|
||||||
"@fontsource/space-mono": "^4.4.5",
|
"devDependencies": {
|
||||||
"@fontsource/ubuntu": "^4.4.5",
|
"@fontsource/atkinson-hyperlegible": "^4.4.5",
|
||||||
"@fontsource/ubuntu-mono": "^4.4.5",
|
"@fontsource/bree-serif": "^4.4.5",
|
||||||
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
"@fontsource/comic-neue": "^4.4.5",
|
||||||
"@preact/preset-vite": "^2.0.0",
|
"@fontsource/fira-code": "^4.4.5",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@fontsource/inter": "^4.4.5",
|
||||||
"@styled-icons/boxicons-logos": "^10.34.0",
|
"@fontsource/lato": "^4.4.5",
|
||||||
"@styled-icons/boxicons-regular": "^10.34.0",
|
"@fontsource/montserrat": "^4.4.5",
|
||||||
"@styled-icons/boxicons-solid": "^10.37.0",
|
"@fontsource/noto-sans": "^4.4.5",
|
||||||
"@styled-icons/simple-icons": "^10.33.0",
|
"@fontsource/open-sans": "^4.4.5",
|
||||||
"@tippyjs/react": "^4.2.5",
|
"@fontsource/poppins": "^4.4.5",
|
||||||
"@traptitech/markdown-it-katex": "^3.4.3",
|
"@fontsource/raleway": "^4.4.5",
|
||||||
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
"@fontsource/roboto": "^4.4.5",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
"@fontsource/roboto-mono": "^4.4.5",
|
||||||
"@types/lodash.defaultsdeep": "^4.6.6",
|
"@fontsource/source-code-pro": "^4.4.5",
|
||||||
"@types/lodash.isequal": "^4.5.5",
|
"@fontsource/space-mono": "^4.4.5",
|
||||||
"@types/markdown-it": "^12.0.2",
|
"@fontsource/ubuntu": "^4.4.5",
|
||||||
"@types/node": "^15.12.4",
|
"@fontsource/ubuntu-mono": "^4.4.5",
|
||||||
"@types/preact-i18n": "^2.3.0",
|
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
||||||
"@types/prismjs": "^1.16.5",
|
"@preact/preset-vite": "^2.0.0",
|
||||||
"@types/react-helmet": "^6.1.1",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@styled-icons/boxicons-logos": "^10.34.0",
|
||||||
"@types/react-scroll": "^1.8.2",
|
"@styled-icons/boxicons-regular": "^10.34.0",
|
||||||
"@types/styled-components": "^5.1.10",
|
"@styled-icons/boxicons-solid": "^10.37.0",
|
||||||
"@types/twemoji": "^12.1.1",
|
"@styled-icons/simple-icons": "^10.33.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
"@tippyjs/react": "^4.2.5",
|
||||||
"@typescript-eslint/parser": "^4.27.0",
|
"@traptitech/markdown-it-katex": "^3.4.3",
|
||||||
"classnames": "^2.3.1",
|
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
||||||
"dayjs": "^1.10.6",
|
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
||||||
"detect-browser": "^5.2.0",
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
||||||
"eslint": "^7.28.0",
|
"@types/lodash.isequal": "^4.5.5",
|
||||||
"eslint-config-preact": "^1.1.4",
|
"@types/markdown-it": "^12.0.2",
|
||||||
"eventemitter3": "^4.0.7",
|
"@types/node": "^15.12.4",
|
||||||
"highlight.js": "^11.0.1",
|
"@types/preact-i18n": "^2.3.0",
|
||||||
"idb": "^6.1.2",
|
"@types/prismjs": "^1.16.5",
|
||||||
"localforage": "^1.9.0",
|
"@types/react-helmet": "^6.1.1",
|
||||||
"lodash.defaultsdeep": "^4.6.1",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"lodash.isequal": "^4.5.0",
|
"@types/react-scroll": "^1.8.2",
|
||||||
"markdown-it": "^12.0.6",
|
"@types/styled-components": "^5.1.10",
|
||||||
"markdown-it-emoji": "^2.0.0",
|
"@types/twemoji": "^12.1.1",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||||
"markdown-it-sup": "^1.0.0",
|
"@typescript-eslint/parser": "^4.27.0",
|
||||||
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
|
"classnames": "^2.3.1",
|
||||||
"mobx": "^6.3.2",
|
"dayjs": "^1.10.6",
|
||||||
"mobx-react-lite": "^3.2.0",
|
"detect-browser": "^5.2.0",
|
||||||
"preact-context-menu": "^0.1.5",
|
"eslint": "^7.28.0",
|
||||||
"preact-i18n": "^2.4.0-preactx",
|
"eslint-config-preact": "^1.1.4",
|
||||||
"prettier": "^2.3.1",
|
"eventemitter3": "^4.0.7",
|
||||||
"prismjs": "^1.23.0",
|
"highlight.js": "^11.0.1",
|
||||||
"react-device-detect": "^1.17.0",
|
"localforage": "^1.9.0",
|
||||||
"react-helmet": "^6.1.0",
|
"lodash.defaultsdeep": "^4.6.1",
|
||||||
"react-hook-form": "6.3.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"react-overlapping-panels": "1.2.2",
|
"markdown-it": "^12.0.6",
|
||||||
"react-redux": "^7.2.4",
|
"markdown-it-emoji": "^2.0.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
"react-scroll": "^1.8.2",
|
"markdown-it-sup": "^1.0.0",
|
||||||
"redux": "^4.1.0",
|
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
|
||||||
"revolt.js": "5.0.0-alpha.18",
|
"mobx": "^6.3.2",
|
||||||
"rimraf": "^3.0.2",
|
"mobx-react-lite": "^3.2.0",
|
||||||
"sass": "^1.35.1",
|
"preact": "^10.5.14",
|
||||||
"shade-blend-color": "^1.0.0",
|
"preact-context-menu": "^0.1.5",
|
||||||
"styled-components": "^5.3.0",
|
"preact-i18n": "^2.4.0-preactx",
|
||||||
"typescript": "^4.3.2",
|
"prettier": "^2.3.1",
|
||||||
"ulid": "^2.3.0",
|
"prismjs": "^1.23.0",
|
||||||
"use-resize-observer": "^7.0.0",
|
"react-device-detect": "^1.17.0",
|
||||||
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b",
|
"react-helmet": "^6.1.0",
|
||||||
"vite-plugin-pwa": "^0.8.1",
|
"react-hook-form": "6.3.0",
|
||||||
"workbox-precaching": "^6.1.5"
|
"react-overlapping-panels": "1.2.2",
|
||||||
}
|
"react-redux": "^7.2.4",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-scroll": "^1.8.2",
|
||||||
|
"redux": "^4.1.0",
|
||||||
|
"revolt-api": "0.5.1-alpha.10-patch.0",
|
||||||
|
"revolt.js": "5.0.0-alpha.18",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"sass": "^1.35.1",
|
||||||
|
"shade-blend-color": "^1.0.0",
|
||||||
|
"styled-components": "^5.3.0",
|
||||||
|
"typescript": "^4.3.2",
|
||||||
|
"ulid": "^2.3.0",
|
||||||
|
"use-resize-observer": "^7.0.0",
|
||||||
|
"vite-plugin-pwa": "^0.8.1",
|
||||||
|
"workbox-precaching": "^6.1.5"
|
||||||
|
},
|
||||||
|
"name": "client",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": "https://gitlab.insrt.uk/revolt/revite.git",
|
||||||
|
"author": "Paul <paulmakles@gmail.com>",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { useStore } from "react-redux";
|
|
||||||
import { SYSTEM_USER_ID } from "revolt.js";
|
import { SYSTEM_USER_ID } from "revolt.js";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
import { User } from "revolt.js/dist/maps/Users";
|
||||||
|
@ -143,14 +142,16 @@ export function useAutoComplete(
|
||||||
) as User[];
|
) as User[];
|
||||||
break;
|
break;
|
||||||
case "TextChannel":
|
case "TextChannel":
|
||||||
const server = channel.server_id;
|
{
|
||||||
users = [...client.members.keys()]
|
const server = channel.server_id;
|
||||||
.map((x) => JSON.parse(x))
|
users = [...client.members.keys()]
|
||||||
.filter((x) => x.server === server)
|
.map((x) => JSON.parse(x))
|
||||||
.map((x) => client.users.get(x.user))
|
.filter((x) => x.server === server)
|
||||||
.filter(
|
.map((x) => client.users.get(x.user))
|
||||||
(x) => typeof x !== "undefined",
|
.filter(
|
||||||
) as User[];
|
(x) => typeof x !== "undefined",
|
||||||
|
) as User[];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
|
@ -304,7 +305,7 @@ export function useAutoComplete(
|
||||||
|
|
||||||
function onKeyUp(e: KeyboardEvent) {
|
function onKeyUp(e: KeyboardEvent) {
|
||||||
if (e.currentTarget !== null) {
|
if (e.currentTarget !== null) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error Type mis-match.
|
||||||
onChange(e);
|
onChange(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -391,6 +392,7 @@ export default function AutoComplete({
|
||||||
{state.type === "emoji" &&
|
{state.type === "emoji" &&
|
||||||
state.matches.map((match, i) => (
|
state.matches.map((match, i) => (
|
||||||
<button
|
<button
|
||||||
|
key={match}
|
||||||
className={i === state.selected ? "active" : ""}
|
className={i === state.selected ? "active" : ""}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
(i !== state.selected || !state.within) &&
|
(i !== state.selected || !state.within) &&
|
||||||
|
@ -422,6 +424,7 @@ export default function AutoComplete({
|
||||||
{state.type === "user" &&
|
{state.type === "user" &&
|
||||||
state.matches.map((match, i) => (
|
state.matches.map((match, i) => (
|
||||||
<button
|
<button
|
||||||
|
key={match}
|
||||||
className={i === state.selected ? "active" : ""}
|
className={i === state.selected ? "active" : ""}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
(i !== state.selected || !state.within) &&
|
(i !== state.selected || !state.within) &&
|
||||||
|
@ -446,6 +449,7 @@ export default function AutoComplete({
|
||||||
{state.type === "channel" &&
|
{state.type === "channel" &&
|
||||||
state.matches.map((match, i) => (
|
state.matches.map((match, i) => (
|
||||||
<button
|
<button
|
||||||
|
key={match}
|
||||||
className={i === state.selected ? "active" : ""}
|
className={i === state.selected ? "active" : ""}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
(i !== state.selected || !state.within) &&
|
(i !== state.selected || !state.within) &&
|
||||||
|
|
|
@ -15,7 +15,11 @@ interface Props extends IconBaseProps<Channel> {
|
||||||
|
|
||||||
export default observer(
|
export default observer(
|
||||||
(
|
(
|
||||||
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
|
props: Props &
|
||||||
|
Omit<
|
||||||
|
JSX.HTMLAttributes<HTMLImageElement>,
|
||||||
|
keyof Props | "children" | "as"
|
||||||
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
|
|
||||||
|
@ -25,8 +29,6 @@ export default observer(
|
||||||
attachment,
|
attachment,
|
||||||
isServerChannel: server,
|
isServerChannel: server,
|
||||||
animate,
|
animate,
|
||||||
children,
|
|
||||||
as,
|
|
||||||
...imgProps
|
...imgProps
|
||||||
} = props;
|
} = props;
|
||||||
const iconURL = client.generateFileURL(
|
const iconURL = client.generateFileURL(
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function LocaleSelector(props: Props) {
|
||||||
{Object.keys(Languages).map((x) => {
|
{Object.keys(Languages).map((x) => {
|
||||||
const l = Languages[x as keyof typeof Languages];
|
const l = Languages[x as keyof typeof Languages];
|
||||||
return (
|
return (
|
||||||
<option value={x}>
|
<option value={x} key={x}>
|
||||||
{l.emoji} {l.display}
|
{l.emoji} {l.display}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,23 +22,19 @@ const ServerText = styled.div`
|
||||||
background: var(--primary-background);
|
background: var(--primary-background);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const fallback = "/assets/group.png";
|
// const fallback = "/assets/group.png";
|
||||||
export default observer(
|
export default observer(
|
||||||
(
|
(
|
||||||
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
|
props: Props &
|
||||||
|
Omit<
|
||||||
|
JSX.HTMLAttributes<HTMLImageElement>,
|
||||||
|
keyof Props | "children" | "as"
|
||||||
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
|
|
||||||
const {
|
const { target, attachment, size, animate, server_name, ...imgProps } =
|
||||||
target,
|
props;
|
||||||
attachment,
|
|
||||||
size,
|
|
||||||
animate,
|
|
||||||
server_name,
|
|
||||||
children,
|
|
||||||
as,
|
|
||||||
...imgProps
|
|
||||||
} = props;
|
|
||||||
const iconURL = client.generateFileURL(
|
const iconURL = client.generateFileURL(
|
||||||
target?.icon ?? attachment,
|
target?.icon ?? attachment,
|
||||||
{ max_side: 256 },
|
{ max_side: 256 },
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function Tooltip(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Tippy content={content} {...tippyProps}>
|
<Tippy content={content} {...tippyProps}>
|
||||||
{/*
|
{/*
|
||||||
// @ts-expect-error */}
|
// @ts-expect-error Type mis-match. */}
|
||||||
<div style={`display: flex;`}>{children}</div>
|
<div style={`display: flex;`}>{children}</div>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Download } from "@styled-icons/boxicons-regular";
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { CloudDownload } from "@styled-icons/boxicons-regular";
|
import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
|
||||||
|
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ const Message = observer(
|
||||||
? (attachContextMenu("Menu", {
|
? (attachContextMenu("Menu", {
|
||||||
user: message.author_id,
|
user: message.author_id,
|
||||||
contextualChannel: message.channel_id,
|
contextualChannel: message.channel_id,
|
||||||
|
// eslint-disable-next-line
|
||||||
}) as any)
|
}) as any)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -73,6 +74,7 @@ const Message = observer(
|
||||||
<div id={message._id}>
|
<div id={message._id}>
|
||||||
{message.reply_ids?.map((message_id, index) => (
|
{message.reply_ids?.map((message_id, index) => (
|
||||||
<MessageReply
|
<MessageReply
|
||||||
|
key={message_id}
|
||||||
index={index}
|
index={index}
|
||||||
id={message_id}
|
id={message_id}
|
||||||
channel={message.channel!}
|
channel={message.channel!}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { decodeTime } from "ulid";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { useDictionary } from "../../../lib/i18n";
|
import { useDictionary } from "../../../lib/i18n";
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
|
||||||
|
|
||||||
import { dayjs } from "../../../context/Locale";
|
import { dayjs } from "../../../context/Locale";
|
||||||
|
|
||||||
|
|
|
@ -141,22 +141,25 @@ export default observer(({ channel }: Props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMessage(content?: string) {
|
const setMessage = useCallback(
|
||||||
setDraft(content ?? "");
|
(content?: string) => {
|
||||||
|
setDraft(content ?? "");
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SET_DRAFT",
|
type: "SET_DRAFT",
|
||||||
channel: channel._id,
|
channel: channel._id,
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "CLEAR_DRAFT",
|
type: "CLEAR_DRAFT",
|
||||||
channel: channel._id,
|
channel: channel._id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[channel._id],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function append(content: string, action: "quote" | "mention") {
|
function append(content: string, action: "quote" | "mention") {
|
||||||
|
@ -175,8 +178,12 @@ export default observer(({ channel }: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return internalSubscribe("MessageBox", "append", append);
|
return internalSubscribe(
|
||||||
}, [draft]);
|
"MessageBox",
|
||||||
|
"append",
|
||||||
|
append as (...args: unknown[]) => void,
|
||||||
|
);
|
||||||
|
}, [draft, setMessage]);
|
||||||
|
|
||||||
async function send() {
|
async function send() {
|
||||||
if (uploadState.type === "uploading" || uploadState.type === "sending")
|
if (uploadState.type === "uploading" || uploadState.type === "sending")
|
||||||
|
@ -344,9 +351,11 @@ export default observer(({ channel }: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [
|
// eslint-disable-next-line
|
||||||
channel._id,
|
const debouncedStopTyping = useCallback(
|
||||||
]);
|
debounce(stopTyping as (...args: unknown[]) => void, 1000),
|
||||||
|
[channel._id],
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
onKeyUp,
|
onKeyUp,
|
||||||
|
@ -478,7 +487,7 @@ export default observer(({ channel }: Props) => {
|
||||||
: channel.channel_type === "SavedMessages"
|
: channel.channel_type === "SavedMessages"
|
||||||
? translate("app.main.channel.message_saved")
|
? translate("app.main.channel.message_saved")
|
||||||
: translate("app.main.channel.message_where", {
|
: translate("app.main.channel.message_where", {
|
||||||
channel_name: channel.name,
|
channel_name: channel.name ?? undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Reply } from "@styled-icons/boxicons-regular";
|
|
||||||
import { File } from "@styled-icons/boxicons-solid";
|
import { File } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
@ -13,8 +12,6 @@ import { useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useRenderState } from "../../../../lib/renderer/Singleton";
|
import { useRenderState } from "../../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import Markdown from "../../../markdown/Markdown";
|
import Markdown from "../../../markdown/Markdown";
|
||||||
import UserShort from "../../user/UserShort";
|
import UserShort from "../../user/UserShort";
|
||||||
import { SystemMessage } from "../SystemMessage";
|
import { SystemMessage } from "../SystemMessage";
|
||||||
|
@ -136,10 +133,6 @@ export const ReplyBase = styled.div<{
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Arrow = styled.div`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const MessageReply = observer(({ index, channel, id }: Props) => {
|
export const MessageReply = observer(({ index, channel, id }: Props) => {
|
||||||
const view = useRenderState(channel._id);
|
const view = useRenderState(channel._id);
|
||||||
if (view?.type !== "RENDER") return null;
|
if (view?.type !== "RENDER") return null;
|
||||||
|
@ -155,7 +148,7 @@ export const MessageReply = observer(({ index, channel, id }: Props) => {
|
||||||
} else {
|
} else {
|
||||||
channel.fetchMessage(id).then(setMessage);
|
channel.fetchMessage(id).then(setMessage);
|
||||||
}
|
}
|
||||||
}, [view.messages]);
|
}, [id, channel, view.messages]);
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return (
|
return (
|
||||||
|
@ -204,9 +197,12 @@ export const MessageReply = observer(({ index, channel, id }: Props) => {
|
||||||
{message.attachments && (
|
{message.attachments && (
|
||||||
<>
|
<>
|
||||||
<File size={16} />
|
<File size={16} />
|
||||||
<em>{message.attachments.length > 1 ?
|
<em>
|
||||||
<Text id="app.main.channel.misc.sent_multiple_files" /> :
|
{message.attachments.length > 1 ? (
|
||||||
<Text id="app.main.channel.misc.sent_file" /> }
|
<Text id="app.main.channel.misc.sent_multiple_files" />
|
||||||
|
) : (
|
||||||
|
<Text id="app.main.channel.misc.sent_file" />
|
||||||
|
)}
|
||||||
</em>
|
</em>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -223,4 +219,4 @@ export const MessageReply = observer(({ index, channel, id }: Props) => {
|
||||||
)}
|
)}
|
||||||
</ReplyBase>
|
</ReplyBase>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default function TextFile({ attachment }: Props) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [content, loading, status]);
|
}, [content, loading, status, attachment._id, attachment.size, url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
|
import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
@ -186,7 +187,9 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
|
||||||
<Container>
|
<Container>
|
||||||
<Carousel>
|
<Carousel>
|
||||||
{state.files.map((file, index) => (
|
{state.files.map((file, index) => (
|
||||||
<>
|
// @ts-expect-error brokey
|
||||||
|
// eslint-disable-next-line react/jsx-no-undef
|
||||||
|
<Fragment key={file.name}>
|
||||||
{index === CAN_UPLOAD_AT_ONCE && <Divider />}
|
{index === CAN_UPLOAD_AT_ONCE && <Divider />}
|
||||||
<FileEntry
|
<FileEntry
|
||||||
index={index}
|
index={index}
|
||||||
|
@ -198,7 +201,7 @@ export default function FilePreview({ state, addFile, removeFile }: Props) {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
{state.type === "attached" && (
|
{state.type === "attached" && (
|
||||||
<EmptyEntry onClick={addFile}>
|
<EmptyEntry onClick={addFile}>
|
||||||
|
|
|
@ -83,9 +83,9 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
||||||
(id) =>
|
(id) =>
|
||||||
replies.length < MAX_REPLIES &&
|
replies.length < MAX_REPLIES &&
|
||||||
!replies.find((x) => x.id === id) &&
|
!replies.find((x) => x.id === id) &&
|
||||||
setReplies([...replies, { id, mention: false }]),
|
setReplies([...replies, { id: id as string, mention: false }]),
|
||||||
);
|
);
|
||||||
}, [replies]);
|
}, [replies, setReplies]);
|
||||||
|
|
||||||
const view = useRenderState(channel);
|
const view = useRenderState(channel);
|
||||||
if (view?.type !== "RENDER") return null;
|
if (view?.type !== "RENDER") return null;
|
||||||
|
@ -116,25 +116,28 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
||||||
<UserShort user={message.author} size={16} />
|
<UserShort user={message.author} size={16} />
|
||||||
</div>
|
</div>
|
||||||
<div class="message">
|
<div class="message">
|
||||||
{message.attachments && (
|
{message.attachments && (
|
||||||
<>
|
<>
|
||||||
<File size={16} />
|
<File size={16} />
|
||||||
<em>{message.attachments.length > 1 ?
|
<em>
|
||||||
<Text id="app.main.channel.misc.sent_multiple_files" /> :
|
{message.attachments.length > 1 ? (
|
||||||
<Text id="app.main.channel.misc.sent_file" /> }
|
<Text id="app.main.channel.misc.sent_multiple_files" />
|
||||||
|
) : (
|
||||||
|
<Text id="app.main.channel.misc.sent_file" />
|
||||||
|
)}
|
||||||
</em>
|
</em>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{message.author_id === SYSTEM_USER_ID ? (
|
{message.author_id === SYSTEM_USER_ID ? (
|
||||||
<SystemMessage message={message} />
|
<SystemMessage message={message} />
|
||||||
) : (
|
) : (
|
||||||
<Markdown
|
<Markdown
|
||||||
disallowBigEmoji
|
disallowBigEmoji
|
||||||
content={(
|
content={(
|
||||||
message.content as string
|
message.content as string
|
||||||
).replace(/\n/g, " ")}
|
).replace(/\n/g, " ")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ReplyBase>
|
</ReplyBase>
|
||||||
<span class="actions">
|
<span class="actions">
|
||||||
|
|
|
@ -5,10 +5,6 @@ import styled from "styled-components";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { connectState } from "../../../../redux/connector";
|
|
||||||
|
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
}
|
}
|
||||||
|
@ -104,6 +100,7 @@ export default observer(({ channel }: Props) => {
|
||||||
<div className="avatars">
|
<div className="avatars">
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<img
|
<img
|
||||||
|
key={user!._id}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
src={user!.generateAvatarURL({ max_side: 256 })}
|
src={user!.generateAvatarURL({ max_side: 256 })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { Embed } from "revolt-api/types/January";
|
import { Embed } from "revolt-api/types/January";
|
||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
|
|
|
@ -5,8 +5,7 @@ import { User } from "revolt.js/dist/maps/Users";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { openContextMenu } from "preact-context-menu";
|
import { openContextMenu } from "preact-context-menu";
|
||||||
import { Text } from "preact-i18n";
|
import { Text, Localizer } from "preact-i18n";
|
||||||
import { Localizer } from "preact-i18n";
|
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
|
|
|
@ -52,20 +52,23 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(
|
export default observer(
|
||||||
(props: Props & Omit<JSX.SVGAttributes<SVGSVGElement>, keyof Props>) => {
|
(
|
||||||
|
props: Props &
|
||||||
|
Omit<
|
||||||
|
JSX.SVGAttributes<SVGSVGElement>,
|
||||||
|
keyof Props | "children" | "as"
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
target,
|
target,
|
||||||
attachment,
|
attachment,
|
||||||
size,
|
size,
|
||||||
voice,
|
|
||||||
status,
|
status,
|
||||||
animate,
|
animate,
|
||||||
mask,
|
mask,
|
||||||
hover,
|
hover,
|
||||||
children,
|
|
||||||
as,
|
|
||||||
...svgProps
|
...svgProps
|
||||||
} = props;
|
} = props;
|
||||||
const iconURL =
|
const iconURL =
|
||||||
|
|
|
@ -31,10 +31,10 @@ export const Username = observer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.roles && member.roles.length > 0) {
|
if (member.roles && member.roles.length > 0) {
|
||||||
let srv = client.servers.get(member._id.server);
|
const srv = client.servers.get(member._id.server);
|
||||||
if (srv?.roles) {
|
if (srv?.roles) {
|
||||||
for (let role of member.roles) {
|
for (const role of member.roles) {
|
||||||
let c = srv.roles[role].colour;
|
const c = srv.roles[role].colour;
|
||||||
if (c) {
|
if (c) {
|
||||||
color = c;
|
color = c;
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -9,7 +9,7 @@ export interface MarkdownProps {
|
||||||
|
|
||||||
export default function Markdown(props: MarkdownProps) {
|
export default function Markdown(props: MarkdownProps) {
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error
|
// @ts-expect-error Typings mis-match.
|
||||||
<Suspense fallback={props.content}>
|
<Suspense fallback={props.content}>
|
||||||
<Renderer {...props} />
|
<Renderer {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import MarkdownKatex from "@traptitech/markdown-it-katex";
|
import MarkdownKatex from "@traptitech/markdown-it-katex";
|
||||||
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
|
import MarkdownSpoilers from "@traptitech/markdown-it-spoiler";
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
// @ts-ignore
|
// @ts-expect-error No typings.
|
||||||
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
|
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
|
||||||
// @ts-ignore
|
// @ts-expect-error No typings.
|
||||||
import MarkdownSub from "markdown-it-sub";
|
import MarkdownSub from "markdown-it-sub";
|
||||||
// @ts-ignore
|
// @ts-expect-error No typings.
|
||||||
import MarkdownSup from "markdown-it-sup";
|
import MarkdownSup from "markdown-it-sup";
|
||||||
import Prism from "prismjs";
|
import Prism from "prismjs";
|
||||||
import "prismjs/themes/prism-tomorrow.css";
|
import "prismjs/themes/prism-tomorrow.css";
|
||||||
import { RE_MENTIONS } from "revolt.js";
|
import { RE_MENTIONS } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Markdown.module.scss";
|
import styles from "./Markdown.module.scss";
|
||||||
import { useCallback, useContext, useRef } from "preact/hooks";
|
import { useCallback, useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { internalEmit } from "../../lib/eventEmitter";
|
import { internalEmit } from "../../lib/eventEmitter";
|
||||||
|
|
||||||
|
@ -95,8 +96,8 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||||
// We replace the message with the mention at the time of render.
|
// We replace the message with the mention at the time of render.
|
||||||
// We don't care if the mention changes.
|
// We don't care if the mention changes.
|
||||||
const newContent = content
|
const newContent = content
|
||||||
.replace(RE_MENTIONS, (sub: string, ...args: any[]) => {
|
.replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
|
||||||
const id = args[0],
|
const id = args[0] as string,
|
||||||
user = client.users.get(id);
|
user = client.users.get(id);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -105,8 +106,8 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||||
|
|
||||||
return sub;
|
return sub;
|
||||||
})
|
})
|
||||||
.replace(RE_CHANNELS, (sub: string, ...args: any[]) => {
|
.replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => {
|
||||||
const id = args[0],
|
const id = args[0] as string,
|
||||||
channel = client.channels.get(id);
|
channel = client.channels.get(id);
|
||||||
|
|
||||||
if (channel?.channel_type === "TextChannel") {
|
if (channel?.channel_type === "TextChannel") {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Wrench, Microphone, VolumeFull } from "@styled-icons/boxicons-solid";
|
import { Wrench } from "@styled-icons/boxicons-solid";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import Tooltip from "../common/Tooltip";
|
|
||||||
import UpdateIndicator from "../common/UpdateIndicator";
|
import UpdateIndicator from "../common/UpdateIndicator";
|
||||||
|
|
||||||
const TitlebarBase = styled.div`
|
const TitlebarBase = styled.div`
|
||||||
|
@ -16,11 +15,12 @@ const TitlebarBase = styled.div`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick {
|
.quick {
|
||||||
color: var(--secondary-foreground);
|
color: var(--secondary-foreground);
|
||||||
|
|
||||||
> div, > div > div {
|
> div,
|
||||||
|
> div > div {
|
||||||
width: var(--titlebar-height) !important;
|
width: var(--titlebar-height) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,9 @@ export function Titlebar() {
|
||||||
stroke-width="1"
|
stroke-width="1"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{window.native.getConfig().build === "dev" && <Wrench size="12.5"/>}
|
{window.native.getConfig().build === "dev" && (
|
||||||
|
<Wrench size="12.5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/*<div class="actions quick">
|
{/*<div class="actions quick">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -121,13 +123,50 @@ export function Titlebar() {
|
||||||
<UpdateIndicator style="titlebar" />
|
<UpdateIndicator style="titlebar" />
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div onClick={window.native.min}>
|
<div onClick={window.native.min}>
|
||||||
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><rect fill="currentColor" width="10" height="1" x="1" y="6"></rect></svg>
|
<svg
|
||||||
|
aria-hidden="false"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12">
|
||||||
|
<rect
|
||||||
|
fill="currentColor"
|
||||||
|
width="10"
|
||||||
|
height="1"
|
||||||
|
x="1"
|
||||||
|
y="6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={window.native.max}>
|
<div onClick={window.native.max}>
|
||||||
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="currentColor"></rect></svg>
|
<svg
|
||||||
|
aria-hidden="false"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12">
|
||||||
|
<rect
|
||||||
|
width="9"
|
||||||
|
height="9"
|
||||||
|
x="1.5"
|
||||||
|
y="1.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={window.native.close} class="error">
|
<div onClick={window.native.close} class="error">
|
||||||
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><polygon fill="currentColor" stroke-width="1" fill-rule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1" style="stroke:currentColor;stroke-width:0.4"></polygon></svg>
|
<svg
|
||||||
|
aria-hidden="false"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12">
|
||||||
|
<polygon
|
||||||
|
fill="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"
|
||||||
|
style="stroke:currentColor;stroke-width:0.4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TitlebarBase>
|
</TitlebarBase>
|
||||||
|
|
|
@ -148,6 +148,7 @@ const HomeSidebar = observer((props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionalLink
|
<ConditionalLink
|
||||||
|
key={x.channel._id}
|
||||||
active={x.channel._id === channel}
|
active={x.channel._id === channel}
|
||||||
to={`/channel/${x.channel._id}`}>
|
to={`/channel/${x.channel._id}`}>
|
||||||
<ChannelButton
|
<ChannelButton
|
||||||
|
|
|
@ -279,6 +279,7 @@ export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionalLink
|
<ConditionalLink
|
||||||
|
key={entry.server._id}
|
||||||
active={active}
|
active={active}
|
||||||
to={`/server/${entry.server._id}${
|
to={`/server/${entry.server._id}${
|
||||||
id ? `/channel/${id}` : ""
|
id ? `/channel/${id}` : ""
|
||||||
|
|
|
@ -7,11 +7,11 @@ import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||||
import PaintCounter from "../../../lib/PaintCounter";
|
import PaintCounter from "../../../lib/PaintCounter";
|
||||||
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { dispatch } from "../../../redux";
|
import { dispatch } from "../../../redux";
|
||||||
import { connectState } from "../../../redux/connector";
|
import { connectState } from "../../../redux/connector";
|
||||||
import { Unreads } from "../../../redux/reducers/unreads";
|
import { Unreads } from "../../../redux/reducers/unreads";
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
|
@ -38,9 +38,9 @@ const ServerBase = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
${isTouchscreenDevice &&
|
${isTouchscreenDevice &&
|
||||||
css`
|
css`
|
||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ServerList = styled.div`
|
const ServerList = styled.div`
|
||||||
|
@ -73,7 +73,7 @@ const ServerSidebar = observer((props: Props) => {
|
||||||
parent: server_id!,
|
parent: server_id!,
|
||||||
child: channel_id!,
|
child: channel_id!,
|
||||||
});
|
});
|
||||||
}, [channel_id]);
|
}, [channel_id, server_id]);
|
||||||
|
|
||||||
const uncategorised = new Set(server.channel_ids);
|
const uncategorised = new Set(server.channel_ids);
|
||||||
const elements = [];
|
const elements = [];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { autorun, isObservableProp, reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
|
|
||||||
import { useLayoutEffect } from "preact/hooks";
|
import { useLayoutEffect } from "preact/hooks";
|
||||||
|
@ -6,16 +6,12 @@ import { useLayoutEffect } from "preact/hooks";
|
||||||
import { dispatch } from "../../../redux";
|
import { dispatch } from "../../../redux";
|
||||||
import { Unreads } from "../../../redux/reducers/unreads";
|
import { Unreads } from "../../../redux/reducers/unreads";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
type UnreadProps = {
|
type UnreadProps = {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
unreads: Unreads;
|
unreads: Unreads;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useUnreads({ channel, unreads }: UnreadProps) {
|
export function useUnreads({ channel, unreads }: UnreadProps) {
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
function checkUnread(target: Channel) {
|
function checkUnread(target: Channel) {
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { useRenderState } from "../../../lib/renderer/Singleton";
|
import { useRenderState } from "../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { Presence } from "revolt-api/types/Users";
|
import { Presence } from "revolt-api/types/Users";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import Members, { Member } from "revolt.js/dist/maps/Members";
|
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
import { Message } from "revolt.js/dist/maps/Messages";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
|
||||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
@ -14,7 +12,6 @@ import { getState } from "../../../redux";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import {
|
import {
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
StatusContext,
|
StatusContext,
|
||||||
useClient,
|
useClient,
|
||||||
|
@ -50,7 +47,6 @@ export const GroupMemberSidebar = observer(
|
||||||
({ channel }: { channel: Channel }) => {
|
({ channel }: { channel: Channel }) => {
|
||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
|
|
||||||
const client = useClient();
|
|
||||||
const members = channel.recipients?.filter(
|
const members = channel.recipients?.filter(
|
||||||
(x) => typeof x !== "undefined",
|
(x) => typeof x !== "undefined",
|
||||||
);
|
);
|
||||||
|
@ -173,9 +169,9 @@ export const ServerMemberSidebar = observer(
|
||||||
if (status === ClientStatus.ONLINE) {
|
if (status === ClientStatus.ONLINE) {
|
||||||
channel.server!.fetchMembers();
|
channel.server!.fetchMembers();
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [status, channel.server]);
|
||||||
|
|
||||||
let users = [...client.members.keys()]
|
const users = [...client.members.keys()]
|
||||||
.map((x) => JSON.parse(x))
|
.map((x) => JSON.parse(x))
|
||||||
.filter((x) => x.server === channel.server_id)
|
.filter((x) => x.server === channel.server_id)
|
||||||
.map((y) => client.users.get(y.user)!)
|
.map((y) => client.users.get(y.user)!)
|
||||||
|
@ -247,7 +243,6 @@ export const ServerMemberSidebar = observer(
|
||||||
function Search({ channel }: { channel: Channel }) {
|
function Search({ channel }: { channel: Channel }) {
|
||||||
if (!getState().experiments.enabled?.includes("search")) return null;
|
if (!getState().experiments.enabled?.includes("search")) return null;
|
||||||
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
type Sort = "Relevance" | "Latest" | "Oldest";
|
type Sort = "Relevance" | "Latest" | "Oldest";
|
||||||
const [sort, setSort] = useState<Sort>("Relevance");
|
const [sort, setSort] = useState<Sort>("Relevance");
|
||||||
|
|
||||||
|
@ -272,6 +267,7 @@ function Search({ channel }: { channel: Channel }) {
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
{["Relevance", "Latest", "Oldest"].map((key) => (
|
{["Relevance", "Latest", "Oldest"].map((key) => (
|
||||||
<Button
|
<Button
|
||||||
|
key={key}
|
||||||
style={{ flex: 1, minWidth: 0 }}
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
compact
|
compact
|
||||||
error={sort === key}
|
error={sort === key}
|
||||||
|
@ -304,7 +300,7 @@ function Search({ channel }: { channel: Channel }) {
|
||||||
href += `/channel/${message.channel_id}/${message._id}`;
|
href += `/channel/${message.channel_id}/${message._id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={href}>
|
<Link to={href} key={message._id}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
margin: "2px",
|
margin: "2px",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import styled, { css, keyframes } from "styled-components";
|
import styled, { css, keyframes } from "styled-components";
|
||||||
|
|
||||||
import { createPortal, useEffect, useState } from "preact/compat";
|
import { createPortal, useCallback, useEffect, useState } from "preact/compat";
|
||||||
|
|
||||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||||
|
|
||||||
|
@ -134,7 +135,7 @@ interface Props {
|
||||||
dontModal?: boolean;
|
dontModal?: boolean;
|
||||||
padding?: boolean;
|
padding?: boolean;
|
||||||
|
|
||||||
onClose: () => void;
|
onClose?: () => void;
|
||||||
actions?: Action[];
|
actions?: Action[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
border?: boolean;
|
border?: boolean;
|
||||||
|
@ -163,12 +164,12 @@ export default function Modal(props: Props) {
|
||||||
|
|
||||||
const [animateClose, setAnimateClose] = useState(false);
|
const [animateClose, setAnimateClose] = useState(false);
|
||||||
isModalClosing = animateClose;
|
isModalClosing = animateClose;
|
||||||
function onClose() {
|
const onClose = useCallback(() => {
|
||||||
setAnimateClose(true);
|
setAnimateClose(true);
|
||||||
setTimeout(() => props.onClose(), 2e2);
|
setTimeout(() => props.onClose?.(), 2e2);
|
||||||
}
|
}, [setAnimateClose, props]);
|
||||||
|
|
||||||
useEffect(() => internalSubscribe("Modal", "close", onClose), []);
|
useEffect(() => internalSubscribe("Modal", "close", onClose), [onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.disallowClosing) return;
|
if (props.disallowClosing) return;
|
||||||
|
@ -181,7 +182,7 @@ export default function Modal(props: Props) {
|
||||||
|
|
||||||
document.body.addEventListener("keydown", keyDown);
|
document.body.addEventListener("keydown", keyDown);
|
||||||
return () => document.body.removeEventListener("keydown", keyDown);
|
return () => document.body.removeEventListener("keydown", keyDown);
|
||||||
}, [props.disallowClosing, props.onClose]);
|
}, [props.disallowClosing, onClose]);
|
||||||
|
|
||||||
const confirmationAction = props.actions?.find(
|
const confirmationAction = props.actions?.find(
|
||||||
(action) => action.confirmation,
|
(action) => action.confirmation,
|
||||||
|
@ -211,8 +212,12 @@ export default function Modal(props: Props) {
|
||||||
{content}
|
{content}
|
||||||
{props.actions && (
|
{props.actions && (
|
||||||
<ModalActions>
|
<ModalActions>
|
||||||
{props.actions.map((x) => (
|
{props.actions.map((x, index) => (
|
||||||
<Button {...x} disabled={props.disabled} />
|
<Button
|
||||||
|
key={index}
|
||||||
|
{...x}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ModalActions>
|
</ModalActions>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default function Tip(
|
||||||
{!hideSeparator && <Separator />}
|
{!hideSeparator && <Separator />}
|
||||||
<TipBase {...tipProps}>
|
<TipBase {...tipProps}>
|
||||||
<InfoCircle size={20} />
|
<InfoCircle size={20} />
|
||||||
<span>{props.children}</span>
|
<span>{children}</span>
|
||||||
</TipBase>
|
</TipBase>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import styled, { css } from "styled-components";
|
||||||
import { Children } from "../../../types/Preact";
|
import { Children } from "../../../types/Preact";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
|
readonly hover?: boolean;
|
||||||
readonly account?: boolean;
|
readonly account?: boolean;
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
readonly largeDescription?: boolean;
|
readonly largeDescription?: boolean;
|
||||||
|
@ -25,8 +26,6 @@ const CategoryBase = styled.div<BaseProps>`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -41,8 +40,6 @@ const CategoryBase = styled.div<BaseProps>`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.largeDescription
|
props.largeDescription
|
||||||
|
@ -66,7 +63,7 @@ const CategoryBase = styled.div<BaseProps>`
|
||||||
}
|
}
|
||||||
|
|
||||||
${(props) =>
|
${(props) =>
|
||||||
typeof props.onClick !== "undefined" &&
|
props.hover &&
|
||||||
css`
|
css`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -80,7 +77,7 @@ const CategoryBase = styled.div<BaseProps>`
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.disabled &&
|
props.disabled &&
|
||||||
css`
|
css`
|
||||||
opacity: .4;
|
opacity: 0.4;
|
||||||
/*.content,
|
/*.content,
|
||||||
.action {
|
.action {
|
||||||
color: var(--tertiary-foreground);
|
color: var(--tertiary-foreground);
|
||||||
|
@ -133,17 +130,19 @@ export default function CategoryButton({
|
||||||
account,
|
account,
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
|
hover,
|
||||||
action,
|
action,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<CategoryBase
|
<CategoryBase
|
||||||
|
hover={hover || typeof onClick !== "undefined"}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
account={account}>
|
account={account}>
|
||||||
{icon}
|
{icon}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div className="title">{children}</div>
|
<div className="title">{children}</div>
|
||||||
|
|
||||||
<div className="description">{description}</div>
|
<div className="description">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
|
@ -159,4 +158,4 @@ export default function CategoryButton({
|
||||||
</div>
|
</div>
|
||||||
</CategoryBase>
|
</CategoryBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import update from "dayjs/plugin/updateLocale";
|
||||||
import defaultsDeep from "lodash.defaultsdeep";
|
import defaultsDeep from "lodash.defaultsdeep";
|
||||||
|
|
||||||
import { IntlProvider } from "preact-i18n";
|
import { IntlProvider } from "preact-i18n";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { connectState } from "../redux/connector";
|
import { connectState } from "../redux/connector";
|
||||||
|
|
||||||
|
@ -165,12 +165,14 @@ export interface Dictionary {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Locale({ children, locale }: Props) {
|
function Locale({ children, locale }: Props) {
|
||||||
const [defns, setDefinition] = useState<Dictionary>(definition as any);
|
const [defns, setDefinition] = useState<Dictionary>(
|
||||||
|
definition as Dictionary,
|
||||||
|
);
|
||||||
|
|
||||||
// Load relevant language information, fallback to English if invalid.
|
// Load relevant language information, fallback to English if invalid.
|
||||||
const lang = Languages[locale] ?? Languages.en;
|
const lang = Languages[locale] ?? Languages.en;
|
||||||
|
|
||||||
function transformLanguage(source: { [key: string]: any }) {
|
function transformLanguage(source: Dictionary) {
|
||||||
// Fallback untranslated strings to English (UK)
|
// Fallback untranslated strings to English (UK)
|
||||||
const obj = defaultsDeep(source, definition);
|
const obj = defaultsDeep(source, definition);
|
||||||
|
|
||||||
|
@ -216,43 +218,46 @@ function Locale({ children, locale }: Props) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadLanguage(locale: string) {
|
const loadLanguage = useCallback(
|
||||||
if (locale === "en") {
|
(locale: string) => {
|
||||||
// If English, make sure to restore everything to defaults.
|
if (locale === "en") {
|
||||||
// Use what we already have.
|
// If English, make sure to restore everything to defaults.
|
||||||
const defn = transformLanguage(definition);
|
// Use what we already have.
|
||||||
setDefinition(defn);
|
const defn = transformLanguage(definition as Dictionary);
|
||||||
dayjs.locale("en");
|
|
||||||
dayjs.updateLocale("en", { calendar: defn.dayjs });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
import(`../../external/lang/${lang.i18n}.json`).then(
|
|
||||||
async (lang_file) => {
|
|
||||||
// Transform the definitions data.
|
|
||||||
const defn = transformLanguage(lang_file.default);
|
|
||||||
|
|
||||||
// Determine and load dayjs locales.
|
|
||||||
const target = lang.dayjs ?? lang.i18n;
|
|
||||||
const dayjs_locale = await import(
|
|
||||||
`../../node_modules/dayjs/esm/locale/${target}.js`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load dayjs locales.
|
|
||||||
dayjs.locale(target, dayjs_locale.default);
|
|
||||||
|
|
||||||
if (defn.dayjs) {
|
|
||||||
// Override dayjs calendar locales with our own.
|
|
||||||
dayjs.updateLocale(target, { calendar: defn.dayjs });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply definition to app.
|
|
||||||
setDefinition(defn);
|
setDefinition(defn);
|
||||||
},
|
dayjs.locale("en");
|
||||||
);
|
dayjs.updateLocale("en", { calendar: defn.dayjs });
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => loadLanguage(locale), [locale, lang]);
|
import(`../../external/lang/${lang.i18n}.json`).then(
|
||||||
|
async (lang_file) => {
|
||||||
|
// Transform the definitions data.
|
||||||
|
const defn = transformLanguage(lang_file.default);
|
||||||
|
|
||||||
|
// Determine and load dayjs locales.
|
||||||
|
const target = lang.dayjs ?? lang.i18n;
|
||||||
|
const dayjs_locale = await import(
|
||||||
|
`../../node_modules/dayjs/esm/locale/${target}.js`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load dayjs locales.
|
||||||
|
dayjs.locale(target, dayjs_locale.default);
|
||||||
|
|
||||||
|
if (defn.dayjs) {
|
||||||
|
// Override dayjs calendar locales with our own.
|
||||||
|
dayjs.updateLocale(target, { calendar: defn.dayjs });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply definition to app.
|
||||||
|
setDefinition(defn);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[lang.dayjs, lang.i18n],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => loadLanguage(locale), [locale, lang, loadLanguage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Apply RTL language format.
|
// Apply RTL language format.
|
||||||
|
|
|
@ -4,8 +4,6 @@ import { createGlobalStyle } from "styled-components";
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
|
||||||
|
|
||||||
import { connectState } from "../redux/connector";
|
import { connectState } from "../redux/connector";
|
||||||
|
|
||||||
import { Children } from "../types/Preact";
|
import { Children } from "../types/Preact";
|
||||||
|
@ -311,17 +309,17 @@ function Theme({ children, options }: Props) {
|
||||||
const font = theme.font ?? DEFAULT_FONT;
|
const font = theme.font ?? DEFAULT_FONT;
|
||||||
root.setProperty("--font", `"${font}"`);
|
root.setProperty("--font", `"${font}"`);
|
||||||
FONTS[font].load();
|
FONTS[font].load();
|
||||||
}, [theme.font]);
|
}, [root, theme.font]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const font = theme.monospaceFont ?? DEFAULT_MONO_FONT;
|
const font = theme.monospaceFont ?? DEFAULT_MONO_FONT;
|
||||||
root.setProperty("--monospace-font", `"${font}"`);
|
root.setProperty("--monospace-font", `"${font}"`);
|
||||||
MONOSPACE_FONTS[font].load();
|
MONOSPACE_FONTS[font].load();
|
||||||
}, [theme.monospaceFont]);
|
}, [root, theme.monospaceFont]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
root.setProperty("--ligatures", options?.ligatures ? "normal" : "none");
|
root.setProperty("--ligatures", options?.ligatures ? "normal" : "none");
|
||||||
}, [options?.ligatures]);
|
}, [root, options?.ligatures]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resize = () =>
|
const resize = () =>
|
||||||
|
@ -330,7 +328,7 @@ function Theme({ children, options }: Props) {
|
||||||
|
|
||||||
window.addEventListener("resize", resize);
|
window.addEventListener("resize", resize);
|
||||||
return () => window.removeEventListener("resize", resize);
|
return () => window.removeEventListener("resize", resize);
|
||||||
}, []);
|
}, [root]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={theme}>
|
<ThemeContext.Provider value={theme}>
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
|
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "preact/hooks";
|
||||||
|
|
||||||
import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
|
import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
|
||||||
import type VoiceClient from "../lib/vortex/VoiceClient";
|
import type VoiceClient from "../lib/vortex/VoiceClient";
|
||||||
|
|
||||||
import { Children } from "../types/Preact";
|
import { Children } from "../types/Preact";
|
||||||
import { SoundContext } from "./Settings";
|
import { SoundContext } from "./Settings";
|
||||||
import { AppContext } from "./revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
export enum VoiceStatus {
|
export enum VoiceStatus {
|
||||||
LOADING = 0,
|
LOADING = 0,
|
||||||
|
@ -45,20 +51,22 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Voice({ children }: Props) {
|
export default function Voice({ children }: Props) {
|
||||||
const revoltClient = useContext(AppContext);
|
|
||||||
const [client, setClient] = useState<VoiceClient | undefined>(undefined);
|
const [client, setClient] = useState<VoiceClient | undefined>(undefined);
|
||||||
const [state, setState] = useState<VoiceState>({
|
const [state, setState] = useState<VoiceState>({
|
||||||
status: VoiceStatus.LOADING,
|
status: VoiceStatus.LOADING,
|
||||||
participants: new Map(),
|
participants: new Map(),
|
||||||
});
|
});
|
||||||
|
|
||||||
function setStatus(status: VoiceStatus, roomId?: string) {
|
const setStatus = useCallback(
|
||||||
setState({
|
(status: VoiceStatus, roomId?: string) => {
|
||||||
status,
|
setState({
|
||||||
roomId: roomId ?? client?.roomId,
|
status,
|
||||||
participants: client?.participants ?? new Map(),
|
roomId: roomId ?? client?.roomId,
|
||||||
});
|
participants: client?.participants ?? new Map(),
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
[client?.participants, client?.roomId],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import("../lib/vortex/VoiceClient")
|
import("../lib/vortex/VoiceClient")
|
||||||
|
@ -76,7 +84,7 @@ export default function Voice({ children }: Props) {
|
||||||
console.error("Failed to load voice library!", err);
|
console.error("Failed to load voice library!", err);
|
||||||
setStatus(VoiceStatus.UNAVAILABLE);
|
setStatus(VoiceStatus.UNAVAILABLE);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [setStatus]);
|
||||||
|
|
||||||
const isConnecting = useRef(false);
|
const isConnecting = useRef(false);
|
||||||
const operations: VoiceOperations = useMemo(() => {
|
const operations: VoiceOperations = useMemo(() => {
|
||||||
|
@ -158,7 +166,7 @@ export default function Voice({ children }: Props) {
|
||||||
return client?.stopProduce(type);
|
return client?.stopProduce(type);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [client, setStatus]);
|
||||||
|
|
||||||
const playSound = useContext(SoundContext);
|
const playSound = useContext(SoundContext);
|
||||||
|
|
||||||
|
@ -200,7 +208,7 @@ export default function Voice({ children }: Props) {
|
||||||
client.removeListener("userStopProduce", stateUpdate);
|
client.removeListener("userStopProduce", stateUpdate);
|
||||||
client.removeListener("close", stateUpdate);
|
client.removeListener("close", stateUpdate);
|
||||||
};
|
};
|
||||||
}, [client, state]);
|
}, [client, state, playSound, setStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VoiceContext.Provider value={state}>
|
<VoiceContext.Provider value={state}>
|
||||||
|
|
|
@ -89,13 +89,16 @@ export type Screen =
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IntermediateContext = createContext({
|
export const IntermediateContext = createContext({
|
||||||
screen: { id: "none" } as Screen,
|
screen: { id: "none" },
|
||||||
focusTaken: false,
|
focusTaken: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const IntermediateActionsContext = createContext({
|
export const IntermediateActionsContext = createContext<{
|
||||||
openScreen: (screen: Screen) => {},
|
openScreen: (screen: Screen) => void;
|
||||||
writeClipboard: (text: string) => {},
|
writeClipboard: (text: string) => void;
|
||||||
|
}>({
|
||||||
|
openScreen: null!,
|
||||||
|
writeClipboard: null!,
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -130,12 +133,20 @@ export default function Intermediate(props: Props) {
|
||||||
const navigate = (path: string) => history.push(path);
|
const navigate = (path: string) => history.push(path);
|
||||||
|
|
||||||
const subs = [
|
const subs = [
|
||||||
internalSubscribe("Intermediate", "openProfile", openProfile),
|
internalSubscribe(
|
||||||
internalSubscribe("Intermediate", "navigate", navigate),
|
"Intermediate",
|
||||||
|
"openProfile",
|
||||||
|
openProfile as (...args: unknown[]) => void,
|
||||||
|
),
|
||||||
|
internalSubscribe(
|
||||||
|
"Intermediate",
|
||||||
|
"navigate",
|
||||||
|
navigate as (...args: unknown[]) => void,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => subs.map((unsub) => unsub());
|
return () => subs.map((unsub) => unsub());
|
||||||
}, []);
|
}, [history]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntermediateContext.Provider value={value}>
|
<IntermediateContext.Provider value={value}>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { SignedOutModal } from "./modals/SignedOut";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
screen: Screen;
|
screen: Screen;
|
||||||
openScreen: (id: any) => void;
|
openScreen: (screen: Screen) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modals({ screen, openScreen }: Props) {
|
export default function Modals({ screen, openScreen }: Props) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
callback(username, true)
|
callback(username, true)
|
||||||
.then(() => onClose())
|
.then(() => onClose())
|
||||||
.catch((err: any) => {
|
.catch((err: unknown) => {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -238,7 +238,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
|
||||||
.then((code) => setCode(code))
|
.then((code) => setCode(code))
|
||||||
.catch((err) => setError(takeError(err)))
|
.catch((err) => setError(takeError(err)))
|
||||||
.finally(() => setProcessing(false));
|
.finally(() => setProcessing(false));
|
||||||
}, []);
|
}, [props.target]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptModal
|
<PromptModal
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn";
|
import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn";
|
||||||
import { EmbedImage } from "revolt-api/types/January";
|
import { EmbedImage } from "revolt-api/types/January";
|
||||||
|
|
||||||
|
|
|
@ -85,11 +85,13 @@ export function ModifyAccountModal({ onClose, field }: Props) {
|
||||||
]}>
|
]}>
|
||||||
{/* Preact / React typing incompatabilities */}
|
{/* Preact / React typing incompatabilities */}
|
||||||
<form
|
<form
|
||||||
onSubmit={
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
handleSubmit(
|
handleSubmit(
|
||||||
onSubmit,
|
onSubmit,
|
||||||
) as JSX.GenericEventHandler<HTMLFormElement>
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
}>
|
)(e as any);
|
||||||
|
}}>
|
||||||
{field === "email" && (
|
{field === "email" && (
|
||||||
<FormField
|
<FormField
|
||||||
type="email"
|
type="email"
|
||||||
|
|
|
@ -42,6 +42,7 @@ export function UserPicker(props: Props) {
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserCheckbox
|
<UserCheckbox
|
||||||
|
key={x._id}
|
||||||
user={x}
|
user={x}
|
||||||
checked={selected.includes(x._id)}
|
checked={selected.includes(x._id)}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
|
|
|
@ -20,7 +20,6 @@ import Preloader from "../../../components/ui/Preloader";
|
||||||
|
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
import Markdown from "../../../components/markdown/Markdown";
|
||||||
import {
|
import {
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
StatusContext,
|
StatusContext,
|
||||||
useClient,
|
useClient,
|
||||||
|
@ -30,7 +29,7 @@ import { useIntermediate } from "../Intermediate";
|
||||||
interface Props {
|
interface Props {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
dummy?: boolean;
|
dummy?: boolean;
|
||||||
onClose: () => void;
|
onClose?: () => void;
|
||||||
dummyProfile?: Profile;
|
dummyProfile?: Profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ export const UserProfile = observer(
|
||||||
|
|
||||||
const user = client.users.get(user_id);
|
const user = client.users.get(user_id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
useEffect(onClose, []);
|
if (onClose) useEffect(onClose, []);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +75,7 @@ export const UserProfile = observer(
|
||||||
if (!user_id) return;
|
if (!user_id) return;
|
||||||
if (typeof profile !== "undefined") setProfile(undefined);
|
if (typeof profile !== "undefined") setProfile(undefined);
|
||||||
if (typeof mutual !== "undefined") setMutual(undefined);
|
if (typeof mutual !== "undefined") setMutual(undefined);
|
||||||
}, [user_id]);
|
}, [user_id, mutual, profile]);
|
||||||
|
|
||||||
if (dummy) {
|
if (dummy) {
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -93,7 +92,7 @@ export const UserProfile = observer(
|
||||||
setMutual(null);
|
setMutual(null);
|
||||||
user.fetchMutual().then(setMutual);
|
user.fetchMutual().then(setMutual);
|
||||||
}
|
}
|
||||||
}, [mutual, status]);
|
}, [mutual, status, dummy, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dummy) return;
|
if (dummy) return;
|
||||||
|
@ -104,12 +103,10 @@ export const UserProfile = observer(
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
|
|
||||||
if (user.permission & UserPermission.ViewProfile) {
|
if (user.permission & UserPermission.ViewProfile) {
|
||||||
user.fetchProfile()
|
user.fetchProfile().then(setProfile);
|
||||||
.then(setProfile)
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [profile, status]);
|
}, [profile, status, dummy, user]);
|
||||||
|
|
||||||
const backgroundURL =
|
const backgroundURL =
|
||||||
profile &&
|
profile &&
|
||||||
|
@ -157,7 +154,7 @@ export const UserProfile = observer(
|
||||||
}>
|
}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose?.();
|
||||||
history.push(`/open/${user_id}`);
|
history.push(`/open/${user_id}`);
|
||||||
}}>
|
}}>
|
||||||
<Envelope size={30} />
|
<Envelope size={30} />
|
||||||
|
@ -168,7 +165,7 @@ export const UserProfile = observer(
|
||||||
{user.relationship === RelationshipStatus.User && (
|
{user.relationship === RelationshipStatus.User && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose?.();
|
||||||
if (dummy) return;
|
if (dummy) return;
|
||||||
history.push(`/settings/profile`);
|
history.push(`/settings/profile`);
|
||||||
}}>
|
}}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Plus, X, XCircle } from "@styled-icons/boxicons-regular";
|
import { Plus } from "@styled-icons/boxicons-regular";
|
||||||
import { Pencil } from "@styled-icons/boxicons-solid";
|
import { Pencil } from "@styled-icons/boxicons-solid";
|
||||||
import Axios, { AxiosRequestConfig } from "axios";
|
import Axios, { AxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
|
@ -147,6 +147,7 @@ export function FileUploader(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.behaviour === "multi" && props.append) {
|
if (props.behaviour === "multi" && props.append) {
|
||||||
|
// eslint-disable-next-line
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// File pasting.
|
// File pasting.
|
||||||
function paste(e: ClipboardEvent) {
|
function paste(e: ClipboardEvent) {
|
||||||
|
@ -210,7 +211,7 @@ export function FileUploader(props: Props) {
|
||||||
document.removeEventListener("dragover", dragover);
|
document.removeEventListener("dragover", dragover);
|
||||||
document.removeEventListener("drop", drop);
|
document.removeEventListener("drop", drop);
|
||||||
};
|
};
|
||||||
}, [props.append]);
|
}, [openScreen, props, props.append]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.style === "icon" || props.style === "banner") {
|
if (props.style === "icon" || props.style === "banner") {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { autorun, reaction } from "mobx";
|
|
||||||
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||||
import { Presence, RelationshipStatus } from "revolt-api/types/Users";
|
import { Presence, RelationshipStatus } from "revolt-api/types/Users";
|
||||||
import { SYSTEM_USER_ID } from "revolt.js";
|
import { SYSTEM_USER_ID } from "revolt.js";
|
||||||
|
@ -6,7 +5,7 @@ import { Message } from "revolt.js/dist/maps/Messages";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
import { User } from "revolt.js/dist/maps/Users";
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
import { useContext, useEffect } from "preact/hooks";
|
import { useCallback, useContext, useEffect } from "preact/hooks";
|
||||||
|
|
||||||
import { useTranslation } from "../../lib/i18n";
|
import { useTranslation } from "../../lib/i18n";
|
||||||
|
|
||||||
|
@ -52,191 +51,206 @@ function Notifier({ options, notifs }: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const playSound = useContext(SoundContext);
|
const playSound = useContext(SoundContext);
|
||||||
|
|
||||||
async function message(msg: Message) {
|
const message = useCallback(
|
||||||
if (msg.author_id === client.user!._id) return;
|
async (msg: Message) => {
|
||||||
if (msg.channel_id === channel_id && document.hasFocus()) return;
|
if (msg.author_id === client.user!._id) return;
|
||||||
if (client.user!.status?.presence === Presence.Busy) return;
|
if (msg.channel_id === channel_id && document.hasFocus()) return;
|
||||||
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
|
if (client.user!.status?.presence === Presence.Busy) return;
|
||||||
|
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
|
||||||
|
|
||||||
const notifState = getNotificationState(notifs, msg.channel!);
|
const notifState = getNotificationState(notifs, msg.channel!);
|
||||||
if (!shouldNotify(notifState, msg, client.user!._id)) return;
|
if (!shouldNotify(notifState, msg, client.user!._id)) return;
|
||||||
|
|
||||||
playSound("message");
|
playSound("message");
|
||||||
if (!showNotification) return;
|
if (!showNotification) return;
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
switch (msg.channel?.channel_type) {
|
switch (msg.channel?.channel_type) {
|
||||||
case "SavedMessages":
|
case "SavedMessages":
|
||||||
return;
|
return;
|
||||||
case "DirectMessage":
|
case "DirectMessage":
|
||||||
title = `@${msg.author?.username}`;
|
title = `@${msg.author?.username}`;
|
||||||
break;
|
break;
|
||||||
case "Group":
|
case "Group":
|
||||||
if (msg.author?._id === SYSTEM_USER_ID) {
|
if (msg.author?._id === SYSTEM_USER_ID) {
|
||||||
title = msg.channel.name;
|
title = msg.channel.name;
|
||||||
} else {
|
} else {
|
||||||
title = `@${msg.author?.username} - ${msg.channel.name}`;
|
title = `@${msg.author?.username} - ${msg.channel.name}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "TextChannel":
|
||||||
|
title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = msg.channel?._id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image;
|
||||||
|
if (msg.attachments) {
|
||||||
|
const imageAttachment = msg.attachments.find(
|
||||||
|
(x) => x.metadata.type === "Image",
|
||||||
|
);
|
||||||
|
if (imageAttachment) {
|
||||||
|
image = client.generateFileURL(imageAttachment, {
|
||||||
|
max_side: 720,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case "TextChannel":
|
|
||||||
title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
title = msg.channel?._id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image;
|
|
||||||
if (msg.attachments) {
|
|
||||||
const imageAttachment = msg.attachments.find(
|
|
||||||
(x) => x.metadata.type === "Image",
|
|
||||||
);
|
|
||||||
if (imageAttachment) {
|
|
||||||
image = client.generateFileURL(imageAttachment, {
|
|
||||||
max_side: 720,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let body, icon;
|
let body, icon;
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === "string") {
|
||||||
body = client.markdownToText(msg.content);
|
body = client.markdownToText(msg.content);
|
||||||
icon = msg.author?.generateAvatarURL({ max_side: 256 });
|
icon = msg.author?.generateAvatarURL({ max_side: 256 });
|
||||||
} else {
|
} else {
|
||||||
const users = client.users;
|
const users = client.users;
|
||||||
switch (msg.content.type) {
|
switch (msg.content.type) {
|
||||||
case "user_added":
|
case "user_added":
|
||||||
case "user_remove":
|
case "user_remove":
|
||||||
{
|
{
|
||||||
let user = users.get(msg.content.id);
|
const user = users.get(msg.content.id);
|
||||||
body = translate(
|
body = translate(
|
||||||
`app.main.channel.system.${
|
`app.main.channel.system.${
|
||||||
msg.content.type === "user_added"
|
msg.content.type === "user_added"
|
||||||
? "added_by"
|
? "added_by"
|
||||||
: "removed_by"
|
: "removed_by"
|
||||||
}`,
|
}`,
|
||||||
{
|
{
|
||||||
user: user?.username,
|
user: user?.username,
|
||||||
other_user: users.get(msg.content.by)?.username,
|
other_user: users.get(msg.content.by)
|
||||||
},
|
?.username,
|
||||||
);
|
},
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "user_joined":
|
|
||||||
case "user_left":
|
|
||||||
case "user_kicked":
|
|
||||||
case "user_banned":
|
|
||||||
{
|
|
||||||
let user = users.get(msg.content.id);
|
|
||||||
body = translate(
|
|
||||||
`app.main.channel.system.${msg.content.type}`,
|
|
||||||
{ user: user?.username },
|
|
||||||
);
|
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "channel_renamed":
|
|
||||||
{
|
|
||||||
let user = users.get(msg.content.by);
|
|
||||||
body = translate(
|
|
||||||
`app.main.channel.system.channel_renamed`,
|
|
||||||
{
|
|
||||||
user: users.get(msg.content.by)?.username,
|
|
||||||
name: msg.content.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "channel_description_changed":
|
|
||||||
case "channel_icon_changed":
|
|
||||||
{
|
|
||||||
let user = users.get(msg.content.by);
|
|
||||||
body = translate(
|
|
||||||
`app.main.channel.system.${msg.content.type}`,
|
|
||||||
{ user: users.get(msg.content.by)?.username },
|
|
||||||
);
|
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const notif = await createNotification(title!, {
|
|
||||||
icon,
|
|
||||||
image,
|
|
||||||
body,
|
|
||||||
timestamp: decodeTime(msg._id),
|
|
||||||
tag: msg.channel?._id,
|
|
||||||
badge: "/assets/icons/android-chrome-512x512.png",
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (notif) {
|
|
||||||
notif.addEventListener("click", () => {
|
|
||||||
window.focus();
|
|
||||||
const id = msg.channel_id;
|
|
||||||
if (id !== channel_id) {
|
|
||||||
const channel = client.channels.get(id);
|
|
||||||
if (channel) {
|
|
||||||
if (channel.channel_type === "TextChannel") {
|
|
||||||
history.push(
|
|
||||||
`/server/${channel.server_id}/channel/${id}`,
|
|
||||||
);
|
);
|
||||||
} else {
|
icon = user?.generateAvatarURL({
|
||||||
history.push(`/channel/${id}`);
|
max_side: 256,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
|
case "user_joined":
|
||||||
|
case "user_left":
|
||||||
|
case "user_kicked":
|
||||||
|
case "user_banned":
|
||||||
|
{
|
||||||
|
const user = users.get(msg.content.id);
|
||||||
|
body = translate(
|
||||||
|
`app.main.channel.system.${msg.content.type}`,
|
||||||
|
{ user: user?.username },
|
||||||
|
);
|
||||||
|
icon = user?.generateAvatarURL({
|
||||||
|
max_side: 256,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "channel_renamed":
|
||||||
|
{
|
||||||
|
const user = users.get(msg.content.by);
|
||||||
|
body = translate(
|
||||||
|
`app.main.channel.system.channel_renamed`,
|
||||||
|
{
|
||||||
|
user: users.get(msg.content.by)?.username,
|
||||||
|
name: msg.content.name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
icon = user?.generateAvatarURL({
|
||||||
|
max_side: 256,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "channel_description_changed":
|
||||||
|
case "channel_icon_changed":
|
||||||
|
{
|
||||||
|
const user = users.get(msg.content.by);
|
||||||
|
body = translate(
|
||||||
|
`app.main.channel.system.${msg.content.type}`,
|
||||||
|
{ user: users.get(msg.content.by)?.username },
|
||||||
|
);
|
||||||
|
icon = user?.generateAvatarURL({
|
||||||
|
max_side: 256,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notif = await createNotification(title!, {
|
||||||
|
icon,
|
||||||
|
image,
|
||||||
|
body,
|
||||||
|
timestamp: decodeTime(msg._id),
|
||||||
|
tag: msg.channel?._id,
|
||||||
|
badge: "/assets/icons/android-chrome-512x512.png",
|
||||||
|
silent: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
notifications[msg.channel_id] = notif;
|
if (notif) {
|
||||||
notif.addEventListener(
|
notif.addEventListener("click", () => {
|
||||||
"close",
|
window.focus();
|
||||||
() => delete notifications[msg.channel_id],
|
const id = msg.channel_id;
|
||||||
);
|
if (id !== channel_id) {
|
||||||
}
|
const channel = client.channels.get(id);
|
||||||
}
|
if (channel) {
|
||||||
|
if (channel.channel_type === "TextChannel") {
|
||||||
async function relationship(user: User) {
|
history.push(
|
||||||
if (client.user?.status?.presence === Presence.Busy) return;
|
`/server/${channel.server_id}/channel/${id}`,
|
||||||
if (!showNotification) return;
|
);
|
||||||
|
} else {
|
||||||
let event;
|
history.push(`/channel/${id}`);
|
||||||
switch (user.relationship) {
|
}
|
||||||
case RelationshipStatus.Incoming:
|
}
|
||||||
event = translate("notifications.sent_request", {
|
}
|
||||||
person: user.username,
|
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
case RelationshipStatus.Friend:
|
|
||||||
event = translate("notifications.now_friends", {
|
|
||||||
person: user.username,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const notif = await createNotification(event, {
|
notifications[msg.channel_id] = notif;
|
||||||
icon: user.generateAvatarURL({ max_side: 256 }),
|
notif.addEventListener(
|
||||||
badge: "/assets/icons/android-chrome-512x512.png",
|
"close",
|
||||||
timestamp: +new Date(),
|
() => delete notifications[msg.channel_id],
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
history,
|
||||||
|
showNotification,
|
||||||
|
translate,
|
||||||
|
channel_id,
|
||||||
|
client,
|
||||||
|
notifs,
|
||||||
|
playSound,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
notif?.addEventListener("click", () => {
|
const relationship = useCallback(
|
||||||
history.push(`/friends`);
|
async (user: User) => {
|
||||||
});
|
if (client.user?.status?.presence === Presence.Busy) return;
|
||||||
}
|
if (!showNotification) return;
|
||||||
|
|
||||||
|
let event;
|
||||||
|
switch (user.relationship) {
|
||||||
|
case RelationshipStatus.Incoming:
|
||||||
|
event = translate("notifications.sent_request", {
|
||||||
|
person: user.username,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case RelationshipStatus.Friend:
|
||||||
|
event = translate("notifications.now_friends", {
|
||||||
|
person: user.username,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notif = await createNotification(event, {
|
||||||
|
icon: user.generateAvatarURL({ max_side: 256 }),
|
||||||
|
badge: "/assets/icons/android-chrome-512x512.png",
|
||||||
|
timestamp: +new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
notif?.addEventListener("click", () => {
|
||||||
|
history.push(`/friends`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[client.user?.status?.presence, history, showNotification, translate],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.addListener("message", message);
|
client.addListener("message", message);
|
||||||
|
@ -246,7 +260,16 @@ function Notifier({ options, notifs }: Props) {
|
||||||
client.removeListener("message", message);
|
client.removeListener("message", message);
|
||||||
client.removeListener("user/relationship", relationship);
|
client.removeListener("user/relationship", relationship);
|
||||||
};
|
};
|
||||||
}, [client, playSound, guild_id, channel_id, showNotification, notifs]);
|
}, [
|
||||||
|
client,
|
||||||
|
playSound,
|
||||||
|
guild_id,
|
||||||
|
channel_id,
|
||||||
|
showNotification,
|
||||||
|
notifs,
|
||||||
|
message,
|
||||||
|
relationship,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function visChange() {
|
function visChange() {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { openDB } from "idb";
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { Client } from "revolt.js";
|
import { Client } from "revolt.js";
|
||||||
import { Route } from "revolt.js/dist/api/routes";
|
import { Route } from "revolt.js/dist/api/routes";
|
||||||
|
|
||||||
|
@ -58,29 +57,6 @@ function Context({ auth, children }: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
let db;
|
|
||||||
try {
|
|
||||||
// Match sw.ts#L23
|
|
||||||
db = await openDB("state", 3, {
|
|
||||||
upgrade(db) {
|
|
||||||
for (const store of [
|
|
||||||
"channels",
|
|
||||||
"servers",
|
|
||||||
"users",
|
|
||||||
"members",
|
|
||||||
]) {
|
|
||||||
db.createObjectStore(store, {
|
|
||||||
keyPath: "_id",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
"Failed to open IndexedDB store, continuing without.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
autoReconnect: false,
|
autoReconnect: false,
|
||||||
apiURL: import.meta.env.VITE_API_URL,
|
apiURL: import.meta.env.VITE_API_URL,
|
||||||
|
@ -146,11 +122,11 @@ function Context({ auth, children }: Props) {
|
||||||
ready: () =>
|
ready: () =>
|
||||||
operations.loggedIn() && typeof client.user !== "undefined",
|
operations.loggedIn() && typeof client.user !== "undefined",
|
||||||
};
|
};
|
||||||
}, [client, auth.active]);
|
}, [client, auth.active, openScreen]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => registerEvents({ operations }, setStatus, client),
|
() => registerEvents({ operations }, setStatus, client),
|
||||||
[client],
|
[client, operations],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -203,6 +179,7 @@ function Context({ auth, children }: Props) {
|
||||||
setStatus(ClientStatus.READY);
|
setStatus(ClientStatus.READY);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (status === ClientStatus.LOADING) {
|
if (status === ClientStatus.LOADING) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ function StateMonitor(props: Props) {
|
||||||
|
|
||||||
client.addListener("message", add);
|
client.addListener("message", add);
|
||||||
return () => client.removeListener("message", add);
|
return () => client.removeListener("message", add);
|
||||||
}, [props.messages]);
|
}, [client, props.messages]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import isEqual from "lodash.isequal";
|
||||||
import { UserSettings } from "revolt-api/types/Sync";
|
import { UserSettings } from "revolt-api/types/Sync";
|
||||||
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||||
|
|
||||||
import { useContext, useEffect } from "preact/hooks";
|
import { useCallback, useContext, useEffect, useMemo } from "preact/hooks";
|
||||||
|
|
||||||
import { dispatch } from "../../redux";
|
import { dispatch } from "../../redux";
|
||||||
import { connectState } from "../../redux/connector";
|
import { connectState } from "../../redux/connector";
|
||||||
|
@ -28,7 +28,7 @@ type Props = {
|
||||||
notifications: Notifications;
|
notifications: Notifications;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastValues: { [key in SyncKeys]?: any } = {};
|
const lastValues: { [key in SyncKeys]?: unknown } = {};
|
||||||
|
|
||||||
export function mapSync(
|
export function mapSync(
|
||||||
packet: UserSettings,
|
packet: UserSettings,
|
||||||
|
@ -78,31 +78,38 @@ function SyncManager(props: Props) {
|
||||||
.syncFetchUnreads()
|
.syncFetchUnreads()
|
||||||
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
|
.then((unreads) => dispatch({ type: "UNREADS_SET", unreads }));
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [client, props.sync?.disabled, status]);
|
||||||
|
|
||||||
function syncChange(key: SyncKeys, data: any) {
|
const syncChange = useCallback(
|
||||||
const timestamp = +new Date();
|
(key: SyncKeys, data: unknown) => {
|
||||||
dispatch({
|
const timestamp = +new Date();
|
||||||
type: "SYNC_SET_REVISION",
|
dispatch({
|
||||||
key,
|
type: "SYNC_SET_REVISION",
|
||||||
timestamp,
|
key,
|
||||||
});
|
timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
client.syncSetSettings(
|
client.syncSetSettings(
|
||||||
{
|
{
|
||||||
[key]: data,
|
[key]: data as string,
|
||||||
},
|
},
|
||||||
timestamp,
|
timestamp,
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
[client],
|
||||||
|
);
|
||||||
|
|
||||||
const disabled = props.sync.disabled ?? [];
|
const disabled = useMemo(
|
||||||
|
() => props.sync.disabled ?? [],
|
||||||
|
[props.sync.disabled],
|
||||||
|
);
|
||||||
for (const [key, object] of [
|
for (const [key, object] of [
|
||||||
["appearance", props.settings.appearance],
|
["appearance", props.settings.appearance],
|
||||||
["theme", props.settings.theme],
|
["theme", props.settings.theme],
|
||||||
["locale", props.locale],
|
["locale", props.locale],
|
||||||
["notifications", props.notifications],
|
["notifications", props.notifications],
|
||||||
] as [SyncKeys, any][]) {
|
] as [SyncKeys, unknown][]) {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disabled.indexOf(key) === -1) {
|
if (disabled.indexOf(key) === -1) {
|
||||||
if (typeof lastValues[key] !== "undefined") {
|
if (typeof lastValues[key] !== "undefined") {
|
||||||
|
@ -113,7 +120,7 @@ function SyncManager(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
lastValues[key] = object;
|
lastValues[key] = object;
|
||||||
}, [disabled, object]);
|
}, [key, syncChange, disabled, object]);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -131,7 +138,7 @@ function SyncManager(props: Props) {
|
||||||
|
|
||||||
client.addListener("packet", onPacket);
|
client.addListener("packet", onPacket);
|
||||||
return () => client.removeListener("packet", onPacket);
|
return () => client.removeListener("packet", onPacket);
|
||||||
}, [disabled, props.sync]);
|
}, [client, disabled, props.sync]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { dispatch } from "../../redux";
|
||||||
|
|
||||||
import { ClientOperations, ClientStatus } from "./RevoltClient";
|
import { ClientOperations, ClientStatus } from "./RevoltClient";
|
||||||
|
|
||||||
export var preventReconnect = false;
|
export let preventReconnect = false;
|
||||||
let preventUntil = 0;
|
let preventUntil = 0;
|
||||||
|
|
||||||
export function setReconnectDisallowed(allowed: boolean) {
|
export function setReconnectDisallowed(allowed: boolean) {
|
||||||
|
@ -34,6 +34,7 @@ export function registerEvents(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let listeners: Record<string, (...args: any[]) => void> = {
|
let listeners: Record<string, (...args: any[]) => void> = {
|
||||||
connecting: () =>
|
connecting: () =>
|
||||||
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
operations.ready() && setStatus(ClientStatus.CONNECTING),
|
||||||
|
@ -74,7 +75,7 @@ export function registerEvents(
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
listeners = new Proxy(listeners, {
|
listeners = new Proxy(listeners, {
|
||||||
get:
|
get:
|
||||||
(target, listener, receiver) =>
|
(target, listener) =>
|
||||||
(...args: unknown[]) => {
|
(...args: unknown[]) => {
|
||||||
console.debug(`Calling ${listener.toString()} with`, args);
|
console.debug(`Calling ${listener.toString()} with`, args);
|
||||||
Reflect.get(target, listener)(...args);
|
Reflect.get(target, listener)(...args);
|
||||||
|
@ -87,10 +88,6 @@ export function registerEvents(
|
||||||
client.addListener(listener, listeners[listener]);
|
client.addListener(listener, listeners[listener]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function logMutation(target: string, key: string) {
|
|
||||||
console.log("(o) Object mutated", target, "\nChanged:", key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const online = () => {
|
const online = () => {
|
||||||
if (operations.ready()) {
|
if (operations.ready()) {
|
||||||
setStatus(ClientStatus.RECONNECTING);
|
setStatus(ClientStatus.RECONNECTING);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Client } from "revolt.js";
|
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { Children } from "../../types/Preact";
|
import { Children } from "../../types/Preact";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function takeError(error: any): string {
|
export function takeError(error: any): string {
|
||||||
const type = error?.response?.data?.type;
|
const type = error?.response?.data?.type;
|
||||||
const id = type;
|
const id = type;
|
||||||
|
|
4
src/globals.d.ts
vendored
4
src/globals.d.ts
vendored
|
@ -18,10 +18,12 @@ declare interface Window {
|
||||||
relaunch();
|
relaunch();
|
||||||
|
|
||||||
getConfig(): NativeConfig;
|
getConfig(): NativeConfig;
|
||||||
set(key: keyof NativeConfig, value: any);
|
set(key: keyof NativeConfig, value: unknown);
|
||||||
|
|
||||||
getAutoStart(): Promise<boolean>;
|
getAutoStart(): Promise<boolean>;
|
||||||
enableAutoStart(): Promise<void>;
|
enableAutoStart(): Promise<void>;
|
||||||
disableAutoStart(): Promise<void>;
|
disableAutoStart(): Promise<void>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare const Fragment = preact.Fragment;
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { Server } from "revolt.js/dist/maps/Servers";
|
||||||
import { User } from "revolt.js/dist/maps/Users";
|
import { User } from "revolt.js/dist/maps/Users";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
|
||||||
ContextMenuWithData,
|
ContextMenuWithData,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
openContextMenu,
|
openContextMenu,
|
||||||
|
@ -42,12 +41,11 @@ import {
|
||||||
} from "../redux/reducers/notifications";
|
} from "../redux/reducers/notifications";
|
||||||
import { QueuedMessage } from "../redux/reducers/queue";
|
import { QueuedMessage } from "../redux/reducers/queue";
|
||||||
|
|
||||||
import { useIntermediate } from "../context/intermediate/Intermediate";
|
import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
|
||||||
import {
|
import {
|
||||||
AppContext,
|
AppContext,
|
||||||
ClientStatus,
|
ClientStatus,
|
||||||
StatusContext,
|
StatusContext,
|
||||||
useClient,
|
|
||||||
} from "../context/revoltjs/RevoltClient";
|
} from "../context/revoltjs/RevoltClient";
|
||||||
import { takeError } from "../context/revoltjs/util";
|
import { takeError } from "../context/revoltjs/util";
|
||||||
|
|
||||||
|
@ -179,7 +177,7 @@ function ContextMenus(props: Props) {
|
||||||
case "retry_message":
|
case "retry_message":
|
||||||
{
|
{
|
||||||
const nonce = data.message.id;
|
const nonce = data.message.id;
|
||||||
const fail = (error: any) =>
|
const fail = (error: string) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "QUEUE_FAIL",
|
type: "QUEUE_FAIL",
|
||||||
nonce,
|
nonce,
|
||||||
|
@ -369,7 +367,8 @@ function ContextMenus(props: Props) {
|
||||||
|
|
||||||
case "clear_status":
|
case "clear_status":
|
||||||
{
|
{
|
||||||
const { text, ...status } = client.user?.status ?? {};
|
const { text: _text, ...status } =
|
||||||
|
client.user?.status ?? {};
|
||||||
await client.users.edit({ status });
|
await client.users.edit({ status });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -382,12 +381,12 @@ function ContextMenus(props: Props) {
|
||||||
case "delete_message":
|
case "delete_message":
|
||||||
case "create_channel":
|
case "create_channel":
|
||||||
case "create_invite":
|
case "create_invite":
|
||||||
// The any here is because typescript flattens the case types into a single type and type structure and specifity is lost or whatever
|
// Typescript flattens the case types into a single type and type structure and specifity is lost
|
||||||
openScreen({
|
openScreen({
|
||||||
id: "special_prompt",
|
id: "special_prompt",
|
||||||
type: data.action,
|
type: data.action,
|
||||||
target: data.target as any,
|
target: data.target,
|
||||||
});
|
} as unknown as Screen);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ban_member":
|
case "ban_member":
|
||||||
|
@ -596,8 +595,11 @@ function ContextMenus(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < actions.length; i++) {
|
for (let i = 0; i < actions.length; i++) {
|
||||||
// The any here is because typescript can't determine that user the actions are linked together correctly
|
// Typescript can't determine that user the actions are linked together correctly
|
||||||
generateAction({ action: actions[i] as any, user });
|
generateAction({
|
||||||
|
action: actions[i],
|
||||||
|
user,
|
||||||
|
} as unknown as Action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -968,6 +970,7 @@ function ContextMenus(props: Props) {
|
||||||
|
|
||||||
const elements: Children[] = [
|
const elements: Children[] = [
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
key="notif"
|
||||||
data={{
|
data={{
|
||||||
action: "set_notification_state",
|
action: "set_notification_state",
|
||||||
key: channel._id,
|
key: channel._id,
|
||||||
|
@ -987,6 +990,7 @@ function ContextMenus(props: Props) {
|
||||||
function generate(key: string, icon: Children) {
|
function generate(key: string, icon: Children) {
|
||||||
elements.push(
|
elements.push(
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
key={key}
|
||||||
data={{
|
data={{
|
||||||
action: "set_notification_state",
|
action: "set_notification_state",
|
||||||
key: channel._id,
|
key: channel._id,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
const counts: { [key: string]: number } = {};
|
const counts: { [key: string]: number } = {};
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { isTouchscreenDevice } from "./isTouchscreenDevice";
|
||||||
|
|
||||||
type TextAreaAutoSizeProps = Omit<
|
type TextAreaAutoSizeProps = Omit<
|
||||||
JSX.HTMLAttributes<HTMLTextAreaElement>,
|
JSX.HTMLAttributes<HTMLTextAreaElement>,
|
||||||
"style" | "value" | "onChange"
|
"style" | "value" | "onChange" | "children" | "as"
|
||||||
> &
|
> &
|
||||||
TextAreaProps & {
|
TextAreaProps & {
|
||||||
forceFocus?: boolean;
|
forceFocus?: boolean;
|
||||||
|
@ -63,8 +63,6 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
||||||
lineHeight,
|
lineHeight,
|
||||||
hideBorder,
|
hideBorder,
|
||||||
forceFocus,
|
forceFocus,
|
||||||
children,
|
|
||||||
as,
|
|
||||||
onChange,
|
onChange,
|
||||||
...textAreaProps
|
...textAreaProps
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -81,7 +79,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTouchscreenDevice) return;
|
if (isTouchscreenDevice) return;
|
||||||
autoFocus && ref.current && ref.current.focus();
|
autoFocus && ref.current && ref.current.focus();
|
||||||
}, [value]);
|
}, [value, autoFocus]);
|
||||||
|
|
||||||
const inputSelected = () =>
|
const inputSelected = () =>
|
||||||
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
|
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
|
||||||
|
@ -114,7 +112,7 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
||||||
|
|
||||||
document.body.addEventListener("keydown", keyDown);
|
document.body.addEventListener("keydown", keyDown);
|
||||||
return () => document.body.removeEventListener("keydown", keyDown);
|
return () => document.body.removeEventListener("keydown", keyDown);
|
||||||
}, [ref]);
|
}, [ref, autoFocus, forceFocus, value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
@ -124,8 +122,12 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return internalSubscribe("TextArea", "focus", focus);
|
return internalSubscribe(
|
||||||
}, [ref]);
|
"TextArea",
|
||||||
|
"focus",
|
||||||
|
focus as (...args: unknown[]) => void,
|
||||||
|
);
|
||||||
|
}, [props.id, ref]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export function urlBase64ToUint8Array(base64String: string) {
|
export function urlBase64ToUint8Array(base64String: string) {
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
const base64 = (base64String + padding)
|
const base64 = (base64String + padding)
|
||||||
.replace(/\-/g, "+")
|
.replace(/-/g, "+")
|
||||||
.replace(/_/g, "/");
|
.replace(/_/g, "/");
|
||||||
const rawData = window.atob(base64);
|
const rawData = window.atob(base64);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export function debounce(cb: Function, duration: number) {
|
export function debounce(cb: (...args: unknown[]) => void, duration: number) {
|
||||||
// Store the timer variable.
|
// Store the timer variable.
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
// This function is given to React.
|
// This function is given to React.
|
||||||
return (...args: any[]) => {
|
return (...args: unknown[]) => {
|
||||||
// Get rid of the old timer.
|
// Get rid of the old timer.
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
// Set a new timer.
|
// Set a new timer.
|
||||||
|
|
|
@ -5,13 +5,13 @@ export const InternalEvent = new EventEmitter();
|
||||||
export function internalSubscribe(
|
export function internalSubscribe(
|
||||||
ns: string,
|
ns: string,
|
||||||
event: string,
|
event: string,
|
||||||
fn: (...args: any[]) => void,
|
fn: (...args: unknown[]) => void,
|
||||||
) {
|
) {
|
||||||
InternalEvent.addListener(`${ns}/${event}`, fn);
|
InternalEvent.addListener(`${ns}/${event}`, fn);
|
||||||
return () => InternalEvent.removeListener(`${ns}/${event}`, fn);
|
return () => InternalEvent.removeListener(`${ns}/${event}`, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function internalEmit(ns: string, event: string, ...args: any[]) {
|
export function internalEmit(ns: string, event: string, ...args: unknown[]) {
|
||||||
InternalEvent.emit(`${ns}/${event}`, ...args);
|
InternalEvent.emit(`${ns}/${event}`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ export function TextReact({ id, fields }: Props) {
|
||||||
const path = id.split(".");
|
const path = id.split(".");
|
||||||
let entry = intl.dictionary[path.shift()!];
|
let entry = intl.dictionary[path.shift()!];
|
||||||
for (const key of path) {
|
for (const key of path) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error TODO: lazy
|
||||||
entry = entry[key];
|
entry = entry[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,8 +56,12 @@ export function TextReact({ id, fields }: Props) {
|
||||||
|
|
||||||
export function useTranslation() {
|
export function useTranslation() {
|
||||||
const { intl } = useContext(IntlContext) as unknown as IntlType;
|
const { intl } = useContext(IntlContext) as unknown as IntlType;
|
||||||
return (id: string, fields?: Object, plural?: number, fallback?: string) =>
|
return (
|
||||||
translate(id, "", intl.dictionary, fields, plural, fallback);
|
id: string,
|
||||||
|
fields?: Record<string, string | undefined>,
|
||||||
|
plural?: number,
|
||||||
|
fallback?: string,
|
||||||
|
) => translate(id, "", intl.dictionary, fields, plural, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDictionary() {
|
export function useDictionary() {
|
||||||
|
|
4
src/lib/js.ts
Normal file
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 EventEmitter3 from "eventemitter3";
|
||||||
import { Client } from "revolt.js";
|
import { Client } from "revolt.js";
|
||||||
import { Message } from "revolt.js/dist/maps/Messages";
|
import { Message } from "revolt.js/dist/maps/Messages";
|
||||||
|
@ -123,6 +124,7 @@ export class SingletonRenderer extends EventEmitter3 {
|
||||||
window
|
window
|
||||||
.getComputedStyle(child)
|
.getComputedStyle(child)
|
||||||
.marginTop.slice(0, -2),
|
.marginTop.slice(0, -2),
|
||||||
|
10,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,6 +169,7 @@ export class SingletonRenderer extends EventEmitter3 {
|
||||||
window
|
window
|
||||||
.getComputedStyle(child)
|
.getComputedStyle(child)
|
||||||
.marginTop.slice(0, -2),
|
.marginTop.slice(0, -2),
|
||||||
|
10,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { noopAsync } from "../../js";
|
||||||
import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
|
import { SMOOTH_SCROLL_ON_RECEIVE } from "../Singleton";
|
||||||
import { RendererRoutines } from "../types";
|
import { RendererRoutines } from "../types";
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ export const SimpleRenderer: RendererRoutines = {
|
||||||
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
|
{ type: "StayAtBottom", smooth: SMOOTH_SCROLL_ON_RECEIVE },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
edit: async () => {},
|
edit: noopAsync,
|
||||||
delete: async (renderer, id) => {
|
delete: async (renderer, id) => {
|
||||||
const channel = renderer.channel;
|
const channel = renderer.channel;
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export const stopPropagation = (
|
export const stopPropagation = (
|
||||||
ev: JSX.TargetedMouseEvent<HTMLElement>,
|
ev: JSX.TargetedMouseEvent<HTMLElement>,
|
||||||
_consume?: any,
|
// eslint-disable-next-line
|
||||||
|
_consume?: unknown,
|
||||||
) => {
|
) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface SignalingEvents {
|
||||||
open: (event: Event) => void;
|
open: (event: Event) => void;
|
||||||
close: (event: CloseEvent) => void;
|
close: (event: CloseEvent) => void;
|
||||||
error: (event: Event) => void;
|
error: (event: Event) => void;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data: (data: any) => void;
|
data: (data: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +88,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
|
||||||
entry(json);
|
entry(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
sendRequest(type: string, data?: any): Promise<any> {
|
sendRequest(type: string, data?: any): Promise<any> {
|
||||||
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
|
if (this.ws === undefined || this.ws.readyState !== WebSocket.OPEN)
|
||||||
return Promise.reject({ error: WSErrorCode.NotConnected });
|
return Promise.reject({ error: WSErrorCode.NotConnected });
|
||||||
|
@ -124,6 +126,7 @@ export default class Signaling extends EventEmitter<SignalingEvents> {
|
||||||
this.index++;
|
this.index++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
|
authenticate(token: string, roomId: string): Promise<AuthenticationResult> {
|
||||||
return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
|
return this.sendRequest(WSCommandType.Authenticate, { token, roomId });
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
|
||||||
|
|
||||||
this.signaling.on(
|
this.signaling.on(
|
||||||
"error",
|
"error",
|
||||||
(error) => {
|
() => {
|
||||||
this.emit("error", new Error("Signaling error"));
|
this.emit("error", new Error("Signaling error"));
|
||||||
},
|
},
|
||||||
this,
|
this,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
@ -44,7 +45,7 @@ export default function Open() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = client.users.get(id);
|
const user = client.users.get(id);
|
||||||
if (user) {
|
if (user) {
|
||||||
const channel: string | undefined = [
|
const channel: string | undefined = [
|
||||||
...client.channels.values(),
|
...client.channels.values(),
|
||||||
|
@ -68,7 +69,7 @@ export default function Open() {
|
||||||
}
|
}
|
||||||
|
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header placement="primary">
|
<Header placement="primary">
|
||||||
|
|
|
@ -16,7 +16,7 @@ export function App() {
|
||||||
<Context>
|
<Context>
|
||||||
<Masks />
|
<Masks />
|
||||||
{/*
|
{/*
|
||||||
// @ts-expect-error */}
|
// @ts-expect-error typings mis-match between preact... and preact? */}
|
||||||
<Suspense fallback={<Preloader type="spinner" />}>
|
<Suspense fallback={<Preloader type="spinner" />}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login">
|
<Route path="/login">
|
||||||
|
|
|
@ -60,11 +60,11 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
||||||
type="channel"
|
type="channel"
|
||||||
channel={channel}
|
channel={channel}
|
||||||
gated={
|
gated={
|
||||||
(channel.channel_type === "TextChannel" ||
|
!!(
|
||||||
channel.channel_type === "Group") &&
|
(channel.channel_type === "TextChannel" ||
|
||||||
channel.name?.includes("nsfw")
|
channel.channel_type === "Group") &&
|
||||||
? true
|
channel.name?.includes("nsfw")
|
||||||
: false
|
)
|
||||||
}>
|
}>
|
||||||
<ChannelHeader
|
<ChannelHeader
|
||||||
channel={channel}
|
channel={channel}
|
||||||
|
@ -110,7 +110,7 @@ function VoiceChannel({ channel }: { channel: ChannelI }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function ChannelComponent() {
|
||||||
const { channel } = useParams<{ channel: string }>();
|
const { channel } = useParams<{ channel: string }>();
|
||||||
return <Channel id={channel} key={channel} />;
|
return <Channel id={channel} key={channel} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import {
|
import {
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Cog,
|
Cog,
|
||||||
|
@ -9,15 +10,12 @@ import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VoiceContext,
|
VoiceContext,
|
||||||
VoiceOperationsContext,
|
VoiceOperationsContext,
|
||||||
VoiceStatus,
|
VoiceStatus,
|
||||||
} from "../../../context/Voice";
|
} from "../../../context/Voice";
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import UpdateIndicator from "../../../components/common/UpdateIndicator";
|
import UpdateIndicator from "../../../components/common/UpdateIndicator";
|
||||||
import IconButton from "../../../components/ui/IconButton";
|
import IconButton from "../../../components/ui/IconButton";
|
||||||
|
|
|
@ -5,6 +5,7 @@ import useResizeObserver from "use-resize-observer";
|
||||||
|
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import {
|
import {
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
|
@ -74,7 +75,7 @@ export function MessageArea({ id }: Props) {
|
||||||
// ? useRef to avoid re-renders
|
// ? useRef to avoid re-renders
|
||||||
const scrollState = useRef<ScrollState>({ type: "Free" });
|
const scrollState = useRef<ScrollState>({ type: "Free" });
|
||||||
|
|
||||||
const setScrollState = (v: ScrollState) => {
|
const setScrollState = useCallback((v: ScrollState) => {
|
||||||
if (v.type === "StayAtBottom") {
|
if (v.type === "StayAtBottom") {
|
||||||
if (scrollState.current.type === "Bottom" || atBottom()) {
|
if (scrollState.current.type === "Bottom" || atBottom()) {
|
||||||
scrollState.current = {
|
scrollState.current = {
|
||||||
|
@ -131,7 +132,7 @@ export function MessageArea({ id }: Props) {
|
||||||
setScrollState({ type: "Free" });
|
setScrollState({ type: "Free" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// ? Determine if we are at the bottom of the scroll container.
|
// ? Determine if we are at the bottom of the scroll container.
|
||||||
// -> https://stackoverflow.com/a/44893438
|
// -> https://stackoverflow.com/a/44893438
|
||||||
|
@ -151,7 +152,7 @@ export function MessageArea({ id }: Props) {
|
||||||
return internalSubscribe("MessageArea", "jump_to_bottom", () =>
|
return internalSubscribe("MessageArea", "jump_to_bottom", () =>
|
||||||
setScrollState({ type: "ScrollToBottom" }),
|
setScrollState({ type: "ScrollToBottom" }),
|
||||||
);
|
);
|
||||||
}, []);
|
}, [setScrollState]);
|
||||||
|
|
||||||
// ? Handle events from renderer.
|
// ? Handle events from renderer.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -163,12 +164,13 @@ export function MessageArea({ id }: Props) {
|
||||||
SingletonMessageRenderer.addListener("scroll", setScrollState);
|
SingletonMessageRenderer.addListener("scroll", setScrollState);
|
||||||
return () =>
|
return () =>
|
||||||
SingletonMessageRenderer.removeListener("scroll", setScrollState);
|
SingletonMessageRenderer.removeListener("scroll", setScrollState);
|
||||||
}, [scrollState]);
|
}, [scrollState, setScrollState]);
|
||||||
|
|
||||||
// ? Load channel initially.
|
// ? Load channel initially.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (message) return;
|
if (message) return;
|
||||||
SingletonMessageRenderer.init(id);
|
SingletonMessageRenderer.init(id);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
// ? If message present or changes, load it as well.
|
// ? If message present or changes, load it as well.
|
||||||
|
@ -184,6 +186,7 @@ export function MessageArea({ id }: Props) {
|
||||||
history.push(`/channel/${id}`);
|
history.push(`/channel/${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [message]);
|
}, [message]);
|
||||||
|
|
||||||
// ? If we are waiting for network, try again.
|
// ? If we are waiting for network, try again.
|
||||||
|
@ -203,11 +206,14 @@ export function MessageArea({ id }: Props) {
|
||||||
SingletonMessageRenderer.markStale();
|
SingletonMessageRenderer.markStale();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [status, state]);
|
}, [id, status, state]);
|
||||||
|
|
||||||
// ? When the container is scrolled.
|
// ? When the container is scrolled.
|
||||||
// ? Also handle StayAtBottom
|
// ? Also handle StayAtBottom
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const current = ref.current;
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
async function onScroll() {
|
async function onScroll() {
|
||||||
if (scrollState.current.type === "Free" && atBottom()) {
|
if (scrollState.current.type === "Free" && atBottom()) {
|
||||||
setScrollState({ type: "Bottom" });
|
setScrollState({ type: "Bottom" });
|
||||||
|
@ -221,12 +227,15 @@ export function MessageArea({ id }: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.current?.addEventListener("scroll", onScroll);
|
current.addEventListener("scroll", onScroll);
|
||||||
return () => ref.current?.removeEventListener("scroll", onScroll);
|
return () => current.removeEventListener("scroll", onScroll);
|
||||||
}, [ref, scrollState]);
|
}, [ref, scrollState, setScrollState]);
|
||||||
|
|
||||||
// ? Top and bottom loaders.
|
// ? Top and bottom loaders.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const current = ref.current;
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
async function onScroll() {
|
async function onScroll() {
|
||||||
if (atTop(100)) {
|
if (atTop(100)) {
|
||||||
SingletonMessageRenderer.loadTop(ref.current!);
|
SingletonMessageRenderer.loadTop(ref.current!);
|
||||||
|
@ -237,12 +246,12 @@ export function MessageArea({ id }: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.current?.addEventListener("scroll", onScroll);
|
current.addEventListener("scroll", onScroll);
|
||||||
return () => ref.current?.removeEventListener("scroll", onScroll);
|
return () => current.removeEventListener("scroll", onScroll);
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
// ? Scroll down whenever the message area resizes.
|
// ? Scroll down whenever the message area resizes.
|
||||||
function stbOnResize() {
|
const stbOnResize = useCallback(() => {
|
||||||
if (!atBottom() && scrollState.current.type === "Bottom") {
|
if (!atBottom() && scrollState.current.type === "Bottom") {
|
||||||
animateScroll.scrollToBottom({
|
animateScroll.scrollToBottom({
|
||||||
container: ref.current,
|
container: ref.current,
|
||||||
|
@ -251,18 +260,18 @@ export function MessageArea({ id }: Props) {
|
||||||
|
|
||||||
setScrollState({ type: "Bottom" });
|
setScrollState({ type: "Bottom" });
|
||||||
}
|
}
|
||||||
}
|
}, [setScrollState]);
|
||||||
|
|
||||||
// ? Scroll down when container resized.
|
// ? Scroll down when container resized.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
stbOnResize();
|
stbOnResize();
|
||||||
}, [height]);
|
}, [stbOnResize, height]);
|
||||||
|
|
||||||
// ? Scroll down whenever the window resizes.
|
// ? Scroll down whenever the window resizes.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
document.addEventListener("resize", stbOnResize);
|
document.addEventListener("resize", stbOnResize);
|
||||||
return () => document.removeEventListener("resize", stbOnResize);
|
return () => document.removeEventListener("resize", stbOnResize);
|
||||||
}, [ref, scrollState]);
|
}, [ref, scrollState, stbOnResize]);
|
||||||
|
|
||||||
// ? Scroll to bottom when pressing 'Escape'.
|
// ? Scroll to bottom when pressing 'Escape'.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -275,7 +284,7 @@ export function MessageArea({ id }: Props) {
|
||||||
|
|
||||||
document.body.addEventListener("keyup", keyUp);
|
document.body.addEventListener("keyup", keyUp);
|
||||||
return () => document.body.removeEventListener("keyup", keyUp);
|
return () => document.body.removeEventListener("keyup", keyUp);
|
||||||
}, [ref, focusTaken]);
|
}, [id, ref, focusTaken]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageAreaWidthContext.Provider
|
<MessageAreaWidthContext.Provider
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
IntermediateContext,
|
IntermediateContext,
|
||||||
useIntermediate,
|
useIntermediate,
|
||||||
} from "../../../context/intermediate/Intermediate";
|
} from "../../../context/intermediate/Intermediate";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import AutoComplete, {
|
import AutoComplete, {
|
||||||
useAutoComplete,
|
useAutoComplete,
|
||||||
|
@ -79,7 +78,7 @@ export default function MessageEditor({ message, finish }: Props) {
|
||||||
|
|
||||||
document.body.addEventListener("keyup", keyUp);
|
document.body.addEventListener("keyup", keyUp);
|
||||||
return () => document.body.removeEventListener("keyup", keyUp);
|
return () => document.body.removeEventListener("keyup", keyUp);
|
||||||
}, [focusTaken]);
|
}, [focusTaken, finish]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { X } from "@styled-icons/boxicons-regular";
|
import { X } from "@styled-icons/boxicons-regular";
|
||||||
import { RelationshipStatus } from "revolt-api/types/Users";
|
import { RelationshipStatus } from "revolt-api/types/Users";
|
||||||
import { SYSTEM_USER_ID } from "revolt.js";
|
import { SYSTEM_USER_ID } from "revolt.js";
|
||||||
import { Message as MessageObject } from "revolt.js/dist/maps/Messages";
|
|
||||||
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
|
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
|
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
|
||||||
import { RenderState } from "../../../lib/renderer/types";
|
import { RenderState } from "../../../lib/renderer/types";
|
||||||
|
@ -17,7 +17,7 @@ import { connectState } from "../../../redux/connector";
|
||||||
import { QueuedMessage } from "../../../redux/reducers/queue";
|
import { QueuedMessage } from "../../../redux/reducers/queue";
|
||||||
|
|
||||||
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
||||||
import { AppContext, useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||||
|
|
||||||
import Message from "../../../components/common/messaging/Message";
|
import Message from "../../../components/common/messaging/Message";
|
||||||
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
|
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
|
||||||
|
@ -76,10 +76,10 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => subs.forEach((unsub) => unsub());
|
return () => subs.forEach((unsub) => unsub());
|
||||||
}, [state.messages]);
|
}, [state.messages, state.type, userId]);
|
||||||
|
|
||||||
let render: Children[] = [],
|
const render: Children[] = [];
|
||||||
previous: MessageObject | undefined;
|
let previous: MessageI | undefined;
|
||||||
|
|
||||||
if (state.atTop) {
|
if (state.atTop) {
|
||||||
render.push(<ConversationStart id={id} />);
|
render.push(<ConversationStart id={id} />);
|
||||||
|
@ -148,30 +148,30 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
highlight={highlight === message._id}
|
highlight={highlight === message._id}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
} else if (
|
||||||
|
message.author?.relationship === RelationshipStatus.Blocked
|
||||||
|
) {
|
||||||
|
blocked++;
|
||||||
} else {
|
} else {
|
||||||
if (message.author?.relationship === RelationshipStatus.Blocked) {
|
if (blocked > 0) pushBlocked();
|
||||||
blocked++;
|
|
||||||
} else {
|
|
||||||
if (blocked > 0) pushBlocked();
|
|
||||||
|
|
||||||
render.push(
|
render.push(
|
||||||
<Message
|
<Message
|
||||||
message={message}
|
message={message}
|
||||||
key={message._id}
|
key={message._id}
|
||||||
head={head}
|
head={head}
|
||||||
content={
|
content={
|
||||||
editing === message._id ? (
|
editing === message._id ? (
|
||||||
<MessageEditor
|
<MessageEditor
|
||||||
message={message}
|
message={message}
|
||||||
finish={stopEditing}
|
finish={stopEditing}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
attachContext
|
attachContext
|
||||||
highlight={highlight === message._id}
|
highlight={highlight === message._id}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
previous = message;
|
previous = message;
|
||||||
|
@ -191,7 +191,7 @@ function MessageRenderer({ id, state, queue, highlight }: Props) {
|
||||||
previous = {
|
previous = {
|
||||||
_id: msg.id,
|
_id: msg.id,
|
||||||
author_id: userId!,
|
author_id: userId!,
|
||||||
} as any;
|
} as MessageI;
|
||||||
}
|
}
|
||||||
|
|
||||||
render.push(
|
render.push(
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { BarChart } from "@styled-icons/boxicons-regular";
|
import { BarChart } from "@styled-icons/boxicons-regular";
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,6 @@ import { stopPropagation } from "../../lib/stopPropagation";
|
||||||
|
|
||||||
import { VoiceOperationsContext } from "../../context/Voice";
|
import { VoiceOperationsContext } from "../../context/Voice";
|
||||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||||
import {
|
|
||||||
AppContext,
|
|
||||||
OperationsContext,
|
|
||||||
} from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import UserIcon from "../../components/common/user/UserIcon";
|
import UserIcon from "../../components/common/user/UserIcon";
|
||||||
import UserStatus from "../../components/common/user/UserStatus";
|
import UserStatus from "../../components/common/user/UserStatus";
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { ChevronRight } from "@styled-icons/boxicons-regular";
|
||||||
ChevronDown,
|
|
||||||
ChevronRight,
|
|
||||||
ListPlus,
|
|
||||||
} from "@styled-icons/boxicons-regular";
|
|
||||||
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
|
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { RelationshipStatus, Presence } from "revolt-api/types/Users";
|
import { RelationshipStatus, Presence } from "revolt-api/types/Users";
|
||||||
|
@ -68,7 +64,9 @@ export default observer(() => {
|
||||||
] as [string, User[], string][];
|
] as [string, User[], string][];
|
||||||
|
|
||||||
const incoming = lists[0][1];
|
const incoming = lists[0][1];
|
||||||
const userlist: Children[] = incoming.map((x) => <b>{x.username}</b>);
|
const userlist: Children[] = incoming.map((x) => (
|
||||||
|
<b key={x._id}>{x.username}</b>
|
||||||
|
));
|
||||||
for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
|
for (let i = incoming.length - 1; i > 0; i--) userlist.splice(i, 0, ", ");
|
||||||
|
|
||||||
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
|
const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0;
|
||||||
|
@ -195,6 +193,7 @@ export default observer(() => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
|
key={section_id}
|
||||||
id={`friends_${section_id}`}
|
id={`friends_${section_id}`}
|
||||||
defaultValue={true}
|
defaultValue={true}
|
||||||
sticky
|
sticky
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Coffee } from "@styled-icons/boxicons-regular";
|
|
||||||
import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
|
import { Home as HomeIcon } from "@styled-icons/boxicons-solid";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default function Invite() {
|
||||||
.then((data) => setInvite(data))
|
.then((data) => setInvite(data))
|
||||||
.catch((err) => setError(takeError(err)));
|
.catch((err) => setError(takeError(err)));
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [client, code, invite, status]);
|
||||||
|
|
||||||
if (typeof invite === "undefined") {
|
if (typeof invite === "undefined") {
|
||||||
return (
|
return (
|
||||||
|
@ -128,7 +128,7 @@ export default function Invite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispose = autorun(() => {
|
const dispose = autorun(() => {
|
||||||
let server = client.servers.get(
|
const server = client.servers.get(
|
||||||
invite.server_id,
|
invite.server_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { UseFormMethods } from "react-hook-form";
|
||||||
|
|
||||||
import { Text, Localizer } from "preact-i18n";
|
import { Text, Localizer } from "preact-i18n";
|
||||||
|
|
||||||
import InputBox from "../../components/ui/InputBox";
|
import InputBox from "../../components/ui/InputBox";
|
||||||
|
@ -6,7 +8,7 @@ import Overline from "../../components/ui/Overline";
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "email" | "username" | "password" | "invite" | "current_password";
|
type: "email" | "username" | "password" | "invite" | "current_password";
|
||||||
showOverline?: boolean;
|
showOverline?: boolean;
|
||||||
register: Function;
|
register: UseFormMethods["register"];
|
||||||
error?: string;
|
error?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
@ -27,9 +29,11 @@ export default function FormField({
|
||||||
)}
|
)}
|
||||||
<Localizer>
|
<Localizer>
|
||||||
<InputBox
|
<InputBox
|
||||||
// Styled uses React typing while we use Preact
|
placeholder={
|
||||||
// this leads to inconsistances where things need to be typed oddly
|
(
|
||||||
placeholder={(<Text id={`login.enter.${type}`} />) as any}
|
<Text id={`login.enter.${type}`} />
|
||||||
|
) as unknown as string
|
||||||
|
}
|
||||||
name={
|
name={
|
||||||
type === "current_password" ? "password" : name ?? type
|
type === "current_password" ? "password" : name ?? type
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ export function CaptchaBlock(props: CaptchaProps) {
|
||||||
if (!client.configuration?.features.captcha.enabled) {
|
if (!client.configuration?.features.captcha.enabled) {
|
||||||
props.onSuccess();
|
props.onSuccess();
|
||||||
}
|
}
|
||||||
}, []);
|
}, [client.configuration?.features.captcha.enabled, props]);
|
||||||
|
|
||||||
if (!client.configuration?.features.captcha.enabled)
|
if (!client.configuration?.features.captcha.enabled)
|
||||||
return <Preloader type="spinner" />;
|
return <Preloader type="spinner" />;
|
||||||
|
|
|
@ -63,7 +63,7 @@ export function Form({ page, callback }: Props) {
|
||||||
setGlobalError(undefined);
|
setGlobalError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
function onError(err: any) {
|
function onError(err: unknown) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
const error = takeError(err);
|
const error = takeError(err);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
|
import { ListCheck, ListUl } from "@styled-icons/boxicons-regular";
|
||||||
import { Route, useHistory, useParams } from "react-router-dom";
|
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@ export default function ChannelSettings() {
|
||||||
const { channel: cid } = useParams<{ channel: string }>();
|
const { channel: cid } = useParams<{ channel: string }>();
|
||||||
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
const history = useHistory();
|
||||||
const channel = client.channels.get(cid);
|
const channel = client.channels.get(cid);
|
||||||
|
|
||||||
if (!channel) return null;
|
if (!channel) return null;
|
||||||
if (
|
if (
|
||||||
channel.channel_type === "SavedMessages" ||
|
channel.channel_type === "SavedMessages" ||
|
||||||
|
@ -24,7 +26,6 @@ export default function ChannelSettings() {
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const history = useHistory();
|
|
||||||
function switchPage(to?: string) {
|
function switchPage(to?: string) {
|
||||||
let base_url;
|
let base_url;
|
||||||
switch (channel?.channel_type) {
|
switch (channel?.channel_type) {
|
||||||
|
@ -67,18 +68,20 @@ export default function ChannelSettings() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
children={[
|
children={
|
||||||
<Route path="/server/:server/channel/:channel/settings/permissions">
|
<Switch>
|
||||||
<Permissions channel={channel} />
|
<Route path="/server/:server/channel/:channel/settings/permissions">
|
||||||
</Route>,
|
<Permissions channel={channel} />
|
||||||
<Route path="/channel/:channel/settings/permissions">
|
</Route>
|
||||||
<Permissions channel={channel} />
|
<Route path="/channel/:channel/settings/permissions">
|
||||||
</Route>,
|
<Permissions channel={channel} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="/">
|
<Route>
|
||||||
<Overview channel={channel} />
|
<Overview channel={channel} />
|
||||||
</Route>,
|
</Route>
|
||||||
]}
|
</Switch>
|
||||||
|
}
|
||||||
category="channel_pages"
|
category="channel_pages"
|
||||||
switchPage={switchPage}
|
switchPage={switchPage}
|
||||||
defaultPage="overview"
|
defaultPage="overview"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { ArrowBack, X } from "@styled-icons/boxicons-regular";
|
import { ArrowBack, X } from "@styled-icons/boxicons-regular";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { Switch, useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import styles from "./Settings.module.scss";
|
import styles from "./Settings.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ export function GenericSettings({
|
||||||
const { page } = useParams<{ page: string }>();
|
const { page } = useParams<{ page: string }>();
|
||||||
|
|
||||||
const [closing, setClosing] = useState(false);
|
const [closing, setClosing] = useState(false);
|
||||||
function exitSettings() {
|
const exitSettings = useCallback(() => {
|
||||||
if (history.length > 1) {
|
if (history.length > 1) {
|
||||||
setClosing(true);
|
setClosing(true);
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export function GenericSettings({
|
||||||
} else {
|
} else {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}
|
}, [history]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function keyDown(e: KeyboardEvent) {
|
function keyDown(e: KeyboardEvent) {
|
||||||
|
@ -72,7 +72,7 @@ export function GenericSettings({
|
||||||
|
|
||||||
document.body.addEventListener("keydown", keyDown);
|
document.body.addEventListener("keydown", keyDown);
|
||||||
return () => document.body.removeEventListener("keydown", keyDown);
|
return () => document.body.removeEventListener("keydown", keyDown);
|
||||||
}, []);
|
}, [exitSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -158,7 +158,9 @@ export function GenericSettings({
|
||||||
<div className={styles.scrollbox}>
|
<div className={styles.scrollbox}>
|
||||||
<div className={styles.contentcontainer}>
|
<div className={styles.contentcontainer}>
|
||||||
{!isTouchscreenDevice &&
|
{!isTouchscreenDevice &&
|
||||||
!pages.find((x) => x.id === page && x.hideTitle) && (
|
!pages.find(
|
||||||
|
(x) => x.id === page && x.hideTitle,
|
||||||
|
) && (
|
||||||
<h1>
|
<h1>
|
||||||
<Text
|
<Text
|
||||||
id={`app.settings.${category}.${
|
id={`app.settings.${category}.${
|
||||||
|
@ -167,19 +169,20 @@ export function GenericSettings({
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
)}
|
)}
|
||||||
<Switch>{children}</Switch>
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{!isTouchscreenDevice && (
|
{!isTouchscreenDevice && (
|
||||||
<div className={styles.action}>
|
<div className={styles.action}>
|
||||||
<div onClick={exitSettings} className={styles.closeButton}>
|
<div
|
||||||
<X size={28} />
|
onClick={exitSettings}
|
||||||
|
className={styles.closeButton}>
|
||||||
|
<X size={28} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ListUl, ListCheck, ListMinus } from "@styled-icons/boxicons-regular";
|
import { ListUl, ListCheck, ListMinus } from "@styled-icons/boxicons-regular";
|
||||||
import { XSquare, Share, Group } from "@styled-icons/boxicons-solid";
|
import { XSquare, Share, Group } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Route, useHistory, useParams } from "react-router-dom";
|
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
@ -77,34 +77,36 @@ export default observer(() => {
|
||||||
hideTitle: true,
|
hideTitle: true,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
children={[
|
children={
|
||||||
<Route path="/server/:server/settings/categories">
|
<Switch>
|
||||||
<Categories server={server} />
|
<Route path="/server/:server/settings/categories">
|
||||||
</Route>,
|
<Categories server={server} />
|
||||||
<Route path="/server/:server/settings/members">
|
</Route>
|
||||||
<RequiresOnline>
|
<Route path="/server/:server/settings/members">
|
||||||
<Members server={server} />
|
<RequiresOnline>
|
||||||
</RequiresOnline>
|
<Members server={server} />
|
||||||
</Route>,
|
</RequiresOnline>
|
||||||
<Route path="/server/:server/settings/invites">
|
</Route>
|
||||||
<RequiresOnline>
|
<Route path="/server/:server/settings/invites">
|
||||||
<Invites server={server} />
|
<RequiresOnline>
|
||||||
</RequiresOnline>
|
<Invites server={server} />
|
||||||
</Route>,
|
</RequiresOnline>
|
||||||
<Route path="/server/:server/settings/bans">
|
</Route>
|
||||||
<RequiresOnline>
|
<Route path="/server/:server/settings/bans">
|
||||||
<Bans server={server} />
|
<RequiresOnline>
|
||||||
</RequiresOnline>
|
<Bans server={server} />
|
||||||
</Route>,
|
</RequiresOnline>
|
||||||
<Route path="/server/:server/settings/roles">
|
</Route>
|
||||||
<RequiresOnline>
|
<Route path="/server/:server/settings/roles">
|
||||||
<Roles server={server} />
|
<RequiresOnline>
|
||||||
</RequiresOnline>
|
<Roles server={server} />
|
||||||
</Route>,
|
</RequiresOnline>
|
||||||
<Route path="/">
|
</Route>
|
||||||
<Overview server={server} />
|
<Route>
|
||||||
</Route>,
|
<Overview server={server} />
|
||||||
]}
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
}
|
||||||
category="server_pages"
|
category="server_pages"
|
||||||
switchPage={switchPage}
|
switchPage={switchPage}
|
||||||
defaultPage="overview"
|
defaultPage="overview"
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
User,
|
User,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
} from "@styled-icons/boxicons-solid";
|
} from "@styled-icons/boxicons-solid";
|
||||||
import { Route, useHistory } from "react-router-dom";
|
import { Route, Switch, useHistory } from "react-router-dom";
|
||||||
import { LIBRARY_VERSION } from "revolt.js";
|
import { LIBRARY_VERSION } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Settings.module.scss";
|
import styles from "./Settings.module.scss";
|
||||||
|
@ -120,103 +120,107 @@ export default function Settings() {
|
||||||
title: <Text id="app.settings.pages.feedback.title" />,
|
title: <Text id="app.settings.pages.feedback.title" />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
children={[
|
children={
|
||||||
<Route path="/settings/profile">
|
<Switch>
|
||||||
<Profile />
|
<Route path="/settings/profile">
|
||||||
</Route>,
|
<Profile />
|
||||||
<Route path="/settings/sessions">
|
</Route>
|
||||||
<RequiresOnline>
|
<Route path="/settings/sessions">
|
||||||
<Sessions />
|
<RequiresOnline>
|
||||||
</RequiresOnline>
|
<Sessions />
|
||||||
</Route>,
|
</RequiresOnline>
|
||||||
<Route path="/settings/appearance">
|
</Route>
|
||||||
<Appearance />
|
<Route path="/settings/appearance">
|
||||||
</Route>,
|
<Appearance />
|
||||||
<Route path="/settings/notifications">
|
</Route>
|
||||||
<Notifications />
|
<Route path="/settings/notifications">
|
||||||
</Route>,
|
<Notifications />
|
||||||
<Route path="/settings/language">
|
</Route>
|
||||||
<Languages />
|
<Route path="/settings/language">
|
||||||
</Route>,
|
<Languages />
|
||||||
<Route path="/settings/sync">
|
</Route>
|
||||||
<Sync />
|
<Route path="/settings/sync">
|
||||||
</Route>,
|
<Sync />
|
||||||
<Route path="/settings/native">
|
</Route>
|
||||||
<Native />
|
<Route path="/settings/native">
|
||||||
</Route>,
|
<Native />
|
||||||
<Route path="/settings/experiments">
|
</Route>
|
||||||
<ExperimentsPage />
|
<Route path="/settings/experiments">
|
||||||
</Route>,
|
<ExperimentsPage />
|
||||||
<Route path="/settings/feedback">
|
</Route>
|
||||||
<Feedback />
|
<Route path="/settings/feedback">
|
||||||
</Route>,
|
<Feedback />
|
||||||
<Route path="/">
|
</Route>
|
||||||
<Account />
|
<Route path="/" exact>
|
||||||
</Route>,
|
<Account />
|
||||||
]}
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
}
|
||||||
defaultPage="account"
|
defaultPage="account"
|
||||||
switchPage={switchPage}
|
switchPage={switchPage}
|
||||||
category="pages"
|
category="pages"
|
||||||
custom={[
|
custom={
|
||||||
<a
|
<>
|
||||||
href="https://gitlab.insrt.uk/revolt"
|
<a
|
||||||
target="_blank"
|
href="https://gitlab.insrt.uk/revolt"
|
||||||
rel="noreferrer">
|
target="_blank"
|
||||||
<ButtonItem compact>
|
rel="noreferrer">
|
||||||
<Gitlab size={20} />
|
<ButtonItem compact>
|
||||||
<Text id="app.settings.pages.source_code" />
|
<Gitlab size={20} />
|
||||||
|
<Text id="app.settings.pages.source_code" />
|
||||||
|
</ButtonItem>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://insrt.uk/donate"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
<ButtonItem className={styles.donate} compact>
|
||||||
|
<Coffee size={20} />
|
||||||
|
<Text id="app.settings.pages.donate.title" />
|
||||||
|
</ButtonItem>
|
||||||
|
</a>
|
||||||
|
<LineDivider />
|
||||||
|
<ButtonItem
|
||||||
|
onClick={() => operations.logout()}
|
||||||
|
className={styles.logOut}
|
||||||
|
compact>
|
||||||
|
<LogOut size={20} />
|
||||||
|
<Text id="app.settings.pages.logOut" />
|
||||||
</ButtonItem>
|
</ButtonItem>
|
||||||
</a>,
|
<div className={styles.version}>
|
||||||
<a
|
<span className={styles.revision}>
|
||||||
href="https://insrt.uk/donate"
|
<a
|
||||||
target="_blank"
|
href={`${REPO_URL}/${GIT_REVISION}`}
|
||||||
rel="noreferrer">
|
target="_blank"
|
||||||
<ButtonItem className={styles.donate} compact>
|
rel="noreferrer">
|
||||||
<Coffee size={20} />
|
{GIT_REVISION.substr(0, 7)}
|
||||||
<Text id="app.settings.pages.donate.title" />
|
</a>
|
||||||
</ButtonItem>
|
{` `}
|
||||||
</a>,
|
<a
|
||||||
<LineDivider />,
|
href={
|
||||||
<ButtonItem
|
GIT_BRANCH !== "DETACHED"
|
||||||
onClick={() => operations.logout()}
|
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
|
||||||
className={styles.logOut}
|
: undefined
|
||||||
compact>
|
}
|
||||||
<LogOut size={20} />
|
target="_blank"
|
||||||
<Text id="app.settings.pages.logOut" />
|
rel="noreferrer">
|
||||||
</ButtonItem>,
|
({GIT_BRANCH})
|
||||||
<div className={styles.version}>
|
</a>
|
||||||
<span className={styles.revision}>
|
</span>
|
||||||
<a
|
<span>
|
||||||
href={`${REPO_URL}/${GIT_REVISION}`}
|
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
|
||||||
target="_blank"
|
{APP_VERSION}
|
||||||
rel="noreferrer">
|
</span>
|
||||||
{GIT_REVISION.substr(0, 7)}
|
{window.isNative && (
|
||||||
</a>
|
<span>Native: {window.nativeVersion}</span>
|
||||||
{` `}
|
)}
|
||||||
<a
|
<span>
|
||||||
href={
|
API: {client.configuration?.revolt ?? "N/A"}
|
||||||
GIT_BRANCH !== "DETACHED"
|
</span>
|
||||||
? `https://gitlab.insrt.uk/revolt/client/-/tree/${GIT_BRANCH}`
|
<span>revolt.js: {LIBRARY_VERSION}</span>
|
||||||
: undefined
|
</div>
|
||||||
}
|
</>
|
||||||
target="_blank"
|
}
|
||||||
rel="noreferrer">
|
|
||||||
({GIT_BRANCH})
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{GIT_BRANCH === "production" ? "Stable" : "Nightly"}{" "}
|
|
||||||
{APP_VERSION}
|
|
||||||
</span>
|
|
||||||
{window.isNative && (
|
|
||||||
<span>Native: {window.nativeVersion}</span>
|
|
||||||
)}
|
|
||||||
<span>
|
|
||||||
API: {client.configuration?.revolt ?? "N/A"}
|
|
||||||
</span>
|
|
||||||
<span>revolt.js: {LIBRARY_VERSION}</span>
|
|
||||||
</div>,
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||||
import styled, { css } from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
|
|
||||||
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
import InputBox from "../../../components/ui/InputBox";
|
||||||
|
@ -31,8 +30,6 @@ const Row = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(({ channel }: Props) => {
|
export default observer(({ channel }: Props) => {
|
||||||
const client = useContext(AppContext);
|
|
||||||
|
|
||||||
const [name, setName] = useState(channel.name ?? undefined);
|
const [name, setName] = useState(channel.name ?? undefined);
|
||||||
const [description, setDescription] = useState(channel.description ?? "");
|
const [description, setDescription] = useState(channel.description ?? "");
|
||||||
|
|
||||||
|
@ -44,7 +41,7 @@ export default observer(({ channel }: Props) => {
|
||||||
|
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
function save() {
|
function save() {
|
||||||
const changes: any = {};
|
const changes: Record<string, string | undefined> = {};
|
||||||
if (name !== channel.name) changes.name = name;
|
if (name !== channel.name) changes.name = name;
|
||||||
if (description !== channel.description)
|
if (description !== channel.description)
|
||||||
changes.description = description;
|
changes.description = description;
|
||||||
|
|
|
@ -64,6 +64,7 @@ export default observer(({ channel }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
key={id}
|
||||||
checked={selected === id}
|
checked={selected === id}
|
||||||
onChange={(selected) => selected && setSelected(id)}>
|
onChange={(selected) => selected && setSelected(id)}>
|
||||||
{role.name}
|
{role.name}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
Trash,
|
Trash,
|
||||||
} from "@styled-icons/boxicons-solid";
|
} from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Link, useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Profile } from "revolt-api/types/Users";
|
import { Profile } from "revolt-api/types/Users";
|
||||||
|
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
|
@ -55,7 +55,7 @@ export const Account = observer(() => {
|
||||||
.user!.fetchProfile()
|
.user!.fetchProfile()
|
||||||
.then((profile) => setProfile(profile ?? {}));
|
.then((profile) => setProfile(profile ?? {}));
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [client, email, profile, status]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.user}>
|
<div className={styles.user}>
|
||||||
|
@ -95,12 +95,17 @@ export const Account = observer(() => {
|
||||||
<div>
|
<div>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
["username", client.user!.username, <At size={24} />],
|
[
|
||||||
["email", email, <Envelope size={24} />],
|
"username",
|
||||||
["password", "•••••••••", <Key size={24} />],
|
client.user!.username,
|
||||||
|
<At key="at" size={24} />,
|
||||||
|
],
|
||||||
|
["email", email, <Envelope key="envelope" size={24} />],
|
||||||
|
["password", "•••••••••", <Key key="key" size={24} />],
|
||||||
] as const
|
] as const
|
||||||
).map(([field, value, icon]) => (
|
).map(([field, value, icon]) => (
|
||||||
<CategoryButton
|
<CategoryButton
|
||||||
|
key={field}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
description={
|
description={
|
||||||
field === "email" ? (
|
field === "email" ? (
|
||||||
|
@ -152,13 +157,15 @@ export const Account = observer(() => {
|
||||||
</h3>
|
</h3>
|
||||||
<h5>
|
<h5>
|
||||||
{/*<Text id="app.settings.pages.account.2fa.description" />*/}
|
{/*<Text id="app.settings.pages.account.2fa.description" />*/}
|
||||||
Two-factor authentication is currently work-in-progress, see {` `}
|
Two-factor authentication is currently work-in-progress, see{" "}
|
||||||
|
{` `}
|
||||||
<a
|
<a
|
||||||
href="https://gitlab.insrt.uk/insert/rauth/-/issues/2"
|
href="https://gitlab.insrt.uk/insert/rauth/-/issues/2"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
tracking issue here
|
tracking issue here
|
||||||
</a>.
|
</a>
|
||||||
|
.
|
||||||
</h5>
|
</h5>
|
||||||
<CategoryButton
|
<CategoryButton
|
||||||
icon={<Lock size={24} color="var(--error)" />}
|
icon={<Lock size={24} color="var(--error)" />}
|
||||||
|
@ -188,7 +195,7 @@ export const Account = observer(() => {
|
||||||
description={
|
description={
|
||||||
"Delete your account, including all of your data."
|
"Delete your account, including all of your data."
|
||||||
}
|
}
|
||||||
onClick={() => {}}
|
hover
|
||||||
action="external">
|
action="external">
|
||||||
<Text id="app.settings.pages.account.manage.delete" />
|
<Text id="app.settings.pages.account.manage.delete" />
|
||||||
</CategoryButton>
|
</CategoryButton>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Reset, Import } from "@styled-icons/boxicons-regular";
|
import { Reset, Import } from "@styled-icons/boxicons-regular";
|
||||||
import { Pencil } from "@styled-icons/boxicons-solid";
|
import { Pencil } from "@styled-icons/boxicons-solid";
|
||||||
// @ts-ignore
|
// @ts-expect-error shade-blend-color does not have typings.
|
||||||
import pSBC from "shade-blend-color";
|
import pSBC from "shade-blend-color";
|
||||||
|
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
|
@ -17,8 +17,10 @@ import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT,
|
DEFAULT_FONT,
|
||||||
DEFAULT_MONO_FONT,
|
DEFAULT_MONO_FONT,
|
||||||
|
Fonts,
|
||||||
FONTS,
|
FONTS,
|
||||||
FONT_KEYS,
|
FONT_KEYS,
|
||||||
|
MonospaceFonts,
|
||||||
MONOSPACE_FONTS,
|
MONOSPACE_FONTS,
|
||||||
MONOSPACE_FONT_KEYS,
|
MONOSPACE_FONT_KEYS,
|
||||||
Theme,
|
Theme,
|
||||||
|
@ -30,6 +32,7 @@ import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import CollapsibleSection from "../../../components/common/CollapsibleSection";
|
import CollapsibleSection from "../../../components/common/CollapsibleSection";
|
||||||
import Tooltip from "../../../components/common/Tooltip";
|
import Tooltip from "../../../components/common/Tooltip";
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
|
import Checkbox from "../../../components/ui/Checkbox";
|
||||||
import ColourSwatches from "../../../components/ui/ColourSwatches";
|
import ColourSwatches from "../../../components/ui/ColourSwatches";
|
||||||
import ComboBox from "../../../components/ui/ComboBox";
|
import ComboBox from "../../../components/ui/ComboBox";
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
import InputBox from "../../../components/ui/InputBox";
|
||||||
|
@ -56,12 +59,12 @@ export function Component(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushOverride(custom: Partial<Theme>) {
|
const pushOverride = useCallback((custom: Partial<Theme>) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "SETTINGS_SET_THEME_OVERRIDE",
|
type: "SETTINGS_SET_THEME_OVERRIDE",
|
||||||
custom,
|
custom,
|
||||||
});
|
});
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
function setAccent(accent: string) {
|
function setAccent(accent: string) {
|
||||||
setOverride({
|
setOverride({
|
||||||
|
@ -80,12 +83,14 @@ export function Component(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
custom: Partial<Theme>,
|
const setOverride = useCallback(
|
||||||
) => void;
|
debounce(pushOverride as (...args: unknown[]) => void, 200),
|
||||||
|
[pushOverride],
|
||||||
|
) as (custom: Partial<Theme>) => void;
|
||||||
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
|
||||||
|
|
||||||
useEffect(() => setOverride({ css }), [css]);
|
useEffect(() => setOverride({ css }), [setOverride, css]);
|
||||||
|
|
||||||
const selected = props.settings.theme?.preset ?? "dark";
|
const selected = props.settings.theme?.preset ?? "dark";
|
||||||
return (
|
return (
|
||||||
|
@ -169,15 +174,15 @@ export function Component(props: Props) {
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value={theme.font ?? DEFAULT_FONT}
|
value={theme.font ?? DEFAULT_FONT}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
pushOverride({ font: e.currentTarget.value as any })
|
pushOverride({ font: e.currentTarget.value as Fonts })
|
||||||
}>
|
}>
|
||||||
{FONT_KEYS.map((key) => (
|
{FONT_KEYS.map((key) => (
|
||||||
<option value={key}>
|
<option value={key} key={key}>
|
||||||
{FONTS[key as keyof typeof FONTS].name}
|
{FONTS[key as keyof typeof FONTS].name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.
|
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.*/}
|
||||||
<p>
|
<p>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={props.settings.theme?.ligatures === true}
|
checked={props.settings.theme?.ligatures === true}
|
||||||
|
@ -191,7 +196,7 @@ export function Component(props: Props) {
|
||||||
}>
|
}>
|
||||||
<Text id="app.settings.pages.appearance.ligatures" />
|
<Text id="app.settings.pages.appearance.ligatures" />
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</p>*/}
|
</p>
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
<Text id="app.settings.pages.appearance.emoji_pack" />
|
<Text id="app.settings.pages.appearance.emoji_pack" />
|
||||||
|
@ -405,11 +410,12 @@ export function Component(props: Props) {
|
||||||
value={theme.monospaceFont ?? DEFAULT_MONO_FONT}
|
value={theme.monospaceFont ?? DEFAULT_MONO_FONT}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
pushOverride({
|
pushOverride({
|
||||||
monospaceFont: e.currentTarget.value as any,
|
monospaceFont: e.currentTarget
|
||||||
|
.value as MonospaceFonts,
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
{MONOSPACE_FONT_KEYS.map((key) => (
|
{MONOSPACE_FONT_KEYS.map((key) => (
|
||||||
<option value={key}>
|
<option value={key} key={key}>
|
||||||
{
|
{
|
||||||
MONOSPACE_FONTS[
|
MONOSPACE_FONTS[
|
||||||
key as keyof typeof MONOSPACE_FONTS
|
key as keyof typeof MONOSPACE_FONTS
|
||||||
|
|
|
@ -23,6 +23,7 @@ export function Component(props: Props) {
|
||||||
</h3>
|
</h3>
|
||||||
{AVAILABLE_EXPERIMENTS.map((key) => (
|
{AVAILABLE_EXPERIMENTS.map((key) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
key={key}
|
||||||
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
|
||||||
onChange={(enabled) =>
|
onChange={(enabled) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
|
@ -70,7 +70,7 @@ export function Feedback() {
|
||||||
placeholder={
|
placeholder={
|
||||||
(
|
(
|
||||||
<Text id="app.settings.pages.feedback.other" />
|
<Text id="app.settings.pages.feedback.other" />
|
||||||
) as any
|
) as unknown as string
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Localizer>
|
</Localizer>
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { SyncOptions } from "../../../redux/reducers/sync";
|
|
||||||
|
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
import Checkbox from "../../../components/ui/Checkbox";
|
import Checkbox from "../../../components/ui/Checkbox";
|
||||||
|
|
||||||
interface Props {
|
export function Native() {
|
||||||
options?: SyncOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Native(props: Props) {
|
|
||||||
const [config, setConfig] = useState(window.native.getConfig());
|
const [config, setConfig] = useState(window.native.getConfig());
|
||||||
const [autoStart, setAutoStart] = useState<boolean | undefined>();
|
const [autoStart, setAutoStart] = useState<boolean | undefined>();
|
||||||
const fetchValue = () => window.native.getAutoStart().then(setAutoStart);
|
const fetchValue = () => window.native.getAutoStart().then(setAutoStart);
|
||||||
|
@ -41,8 +35,7 @@ export function Native(props: Props) {
|
||||||
description="Launch Revolt when you log into your computer.">
|
description="Launch Revolt when you log into your computer.">
|
||||||
Start with computer
|
Start with computer
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={config.discordRPC}
|
checked={config.discordRPC}
|
||||||
onChange={(discordRPC) => {
|
onChange={(discordRPC) => {
|
||||||
|
@ -181,4 +174,4 @@ export function Native(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,7 @@ export function Component({ options }: Props) {
|
||||||
</h3>
|
</h3>
|
||||||
{SOUNDS_ARRAY.map((key) => (
|
{SOUNDS_ARRAY.map((key) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
key={key}
|
||||||
checked={!!enabledSounds[key]}
|
checked={!!enabledSounds[key]}
|
||||||
onChange={(enabled) =>
|
onChange={(enabled) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import { Profile as ProfileI } from "revolt-api/types/Users";
|
||||||
|
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
import { IntlContext, Text, translate } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
|
import { useTranslation } from "../../../lib/i18n";
|
||||||
|
|
||||||
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
|
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
|
||||||
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
||||||
|
@ -16,31 +19,27 @@ import AutoComplete, {
|
||||||
useAutoComplete,
|
useAutoComplete,
|
||||||
} from "../../../components/common/AutoComplete";
|
} from "../../../components/common/AutoComplete";
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
import { Profile } from "revolt-api/types/Users";
|
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const { intl } = useContext(IntlContext);
|
|
||||||
const status = useContext(StatusContext);
|
const status = useContext(StatusContext);
|
||||||
|
const translate = useTranslation();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const [profile, setProfile] = useState<undefined | Profile>(
|
const [profile, setProfile] = useState<undefined | ProfileI>(undefined);
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ! FIXME: temporary solution
|
// ! FIXME: temporary solution
|
||||||
// ! we should just announce profile changes through WS
|
// ! we should just announce profile changes through WS
|
||||||
function refreshProfile() {
|
const refreshProfile = useCallback(() => {
|
||||||
client
|
client
|
||||||
.user!.fetchProfile()
|
.user!.fetchProfile()
|
||||||
.then((profile) => setProfile(profile ?? {}));
|
.then((profile) => setProfile(profile ?? {}));
|
||||||
}
|
}, [client.user, setProfile]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profile === undefined && status === ClientStatus.ONLINE) {
|
if (profile === undefined && status === ClientStatus.ONLINE) {
|
||||||
refreshProfile();
|
refreshProfile();
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [profile, status, refreshProfile]);
|
||||||
|
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
function setContent(content?: string) {
|
function setContent(content?: string) {
|
||||||
|
@ -69,7 +68,6 @@ export function Profile() {
|
||||||
user_id={client.user!._id}
|
user_id={client.user!._id}
|
||||||
dummy={true}
|
dummy={true}
|
||||||
dummyProfile={profile}
|
dummyProfile={profile}
|
||||||
onClose={() => {}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
|
@ -85,9 +83,7 @@ export function Profile() {
|
||||||
behaviour="upload"
|
behaviour="upload"
|
||||||
maxFileSize={4_000_000}
|
maxFileSize={4_000_000}
|
||||||
onUpload={(avatar) => client.users.edit({ avatar })}
|
onUpload={(avatar) => client.users.edit({ avatar })}
|
||||||
remove={() =>
|
remove={() => client.users.edit({ remove: "Avatar" })}
|
||||||
client.users.edit({ remove: "Avatar" })
|
|
||||||
}
|
|
||||||
defaultPreview={client.user!.generateAvatarURL(
|
defaultPreview={client.user!.generateAvatarURL(
|
||||||
{ max_side: 256 },
|
{ max_side: 256 },
|
||||||
true,
|
true,
|
||||||
|
@ -152,8 +148,6 @@ export function Profile() {
|
||||||
? "fetching"
|
? "fetching"
|
||||||
: "placeholder"
|
: "placeholder"
|
||||||
}`,
|
}`,
|
||||||
"",
|
|
||||||
(intl as any).dictionary as Record<string, unknown>,
|
|
||||||
)}
|
)}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export function Sessions() {
|
||||||
);
|
);
|
||||||
setSessions(data);
|
setSessions(data);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [client, setSessions, deviceId]);
|
||||||
|
|
||||||
if (typeof sessions === "undefined") {
|
if (typeof sessions === "undefined") {
|
||||||
return (
|
return (
|
||||||
|
@ -123,6 +123,7 @@ export function Sessions() {
|
||||||
const systemIcon = getSystemIcon(session);
|
const systemIcon = getSystemIcon(session);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
key={session.id}
|
||||||
className={styles.entry}
|
className={styles.entry}
|
||||||
data-active={session.id === deviceId}
|
data-active={session.id === deviceId}
|
||||||
data-deleting={
|
data-deleting={
|
||||||
|
|
|
@ -26,6 +26,7 @@ export function Component(props: Props) {
|
||||||
] as [SyncKeys, string][]
|
] as [SyncKeys, string][]
|
||||||
).map(([key, title]) => (
|
).map(([key, title]) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
key={key}
|
||||||
checked={
|
checked={
|
||||||
(props.options?.disabled ?? []).indexOf(key) === -1
|
(props.options?.disabled ?? []).indexOf(key) === -1
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,7 @@ import { Server } from "revolt.js/dist/maps/Servers";
|
||||||
|
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
import UserIcon from "../../../components/common/user/UserIcon";
|
||||||
import IconButton from "../../../components/ui/IconButton";
|
import IconButton from "../../../components/ui/IconButton";
|
||||||
|
@ -18,15 +16,14 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Bans = observer(({ server }: Props) => {
|
export const Bans = observer(({ server }: Props) => {
|
||||||
const client = useContext(AppContext);
|
|
||||||
const [deleting, setDelete] = useState<string[]>([]);
|
const [deleting, setDelete] = useState<string[]>([]);
|
||||||
const [data, setData] = useState<
|
const [data, setData] = useState<
|
||||||
Route<"GET", "/servers/id/bans">["response"] | undefined
|
Route<"GET", "/servers/id/bans">["response"] | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
server.fetchBans().then(setData as any);
|
server.fetchBans().then(setData);
|
||||||
}, []);
|
}, [server, setData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.userList}>
|
<div className={styles.userList}>
|
||||||
|
@ -43,10 +40,11 @@ export const Bans = observer(({ server }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
{typeof data === "undefined" && <Preloader type="ring" />}
|
{typeof data === "undefined" && <Preloader type="ring" />}
|
||||||
{data?.bans.map((x) => {
|
{data?.bans.map((x) => {
|
||||||
let user = data.users.find((y) => y._id === x._id.user);
|
const user = data.users.find((y) => y._id === x._id.user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
key={x._id.user}
|
||||||
className={styles.ban}
|
className={styles.ban}
|
||||||
data-deleting={deleting.indexOf(x._id.user) > -1}>
|
data-deleting={deleting.indexOf(x._id.user) > -1}>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -4,17 +4,12 @@ import { Category } from "revolt-api/types/Servers";
|
||||||
import { Server } from "revolt.js/dist/maps/Servers";
|
import { Server } from "revolt.js/dist/maps/Servers";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import ChannelIcon from "../../../components/common/ChannelIcon";
|
import ChannelIcon from "../../../components/common/ChannelIcon";
|
||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
import ComboBox from "../../../components/ui/ComboBox";
|
import ComboBox from "../../../components/ui/ComboBox";
|
||||||
import IconButton from "../../../components/ui/IconButton";
|
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
import InputBox from "../../../components/ui/InputBox";
|
||||||
import Preloader from "../../../components/ui/Preloader";
|
|
||||||
import Tip from "../../../components/ui/Tip";
|
import Tip from "../../../components/ui/Tip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -23,7 +18,6 @@ interface Props {
|
||||||
|
|
||||||
// ! FIXME: really bad code
|
// ! FIXME: really bad code
|
||||||
export const Categories = observer(({ server }: Props) => {
|
export const Categories = observer(({ server }: Props) => {
|
||||||
const client = useContext(AppContext);
|
|
||||||
const channels = server.channels.filter((x) => typeof x !== "undefined");
|
const channels = server.channels.filter((x) => typeof x !== "undefined");
|
||||||
|
|
||||||
const [cats, setCats] = useState<Category[]>(server.categories ?? []);
|
const [cats, setCats] = useState<Category[]>(server.categories ?? []);
|
||||||
|
@ -95,6 +89,7 @@ export const Categories = observer(({ server }: Props) => {
|
||||||
{channels.map((channel) => {
|
{channels.map((channel) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
key={channel!._id}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "12px",
|
gap: "12px",
|
||||||
|
@ -131,7 +126,9 @@ export const Categories = observer(({ server }: Props) => {
|
||||||
}>
|
}>
|
||||||
<option value="none">Uncategorised</option>
|
<option value="none">Uncategorised</option>
|
||||||
{cats.map((x) => (
|
{cats.map((x) => (
|
||||||
<option value={x.id}>{x.title}</option>
|
<option key={x.id} value={x.id}>
|
||||||
|
{x.title}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const Invites = observer(({ server }: Props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
server.fetchInvites().then(setInvites);
|
server.fetchInvites().then(setInvites);
|
||||||
}, []);
|
}, [server, setInvites]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.userList}>
|
<div className={styles.userList}>
|
||||||
|
@ -57,6 +57,7 @@ export const Invites = observer(({ server }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
key={invite._id}
|
||||||
className={styles.invite}
|
className={styles.invite}
|
||||||
data-deleting={deleting.indexOf(invite._id) > -1}>
|
data-deleting={deleting.indexOf(invite._id) > -1}>
|
||||||
<code>{invite._id}</code>
|
<code>{invite._id}</code>
|
||||||
|
|
|
@ -9,8 +9,6 @@ import styles from "./Panes.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
import UserIcon from "../../../components/common/user/UserIcon";
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
import Checkbox from "../../../components/ui/Checkbox";
|
import Checkbox from "../../../components/ui/Checkbox";
|
||||||
|
@ -29,7 +27,7 @@ export const Members = observer(({ server }: Props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
server.fetchMembers().then(setData);
|
server.fetchMembers().then(setData);
|
||||||
}, []);
|
}, [server, setData]);
|
||||||
|
|
||||||
const [roles, setRoles] = useState<string[]>([]);
|
const [roles, setRoles] = useState<string[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -38,7 +36,7 @@ export const Members = observer(({ server }: Props) => {
|
||||||
data!.members.find((x) => x._id.user === selected)?.roles ?? [],
|
data!.members.find((x) => x._id.user === selected)?.roles ?? [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [selected]);
|
}, [setRoles, selected, data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.userList}>
|
<div className={styles.userList}>
|
||||||
|
@ -57,9 +55,10 @@ export const Members = observer(({ server }: Props) => {
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.map(({ member, user }) => (
|
.map(({ member, user }) => (
|
||||||
<>
|
// @ts-expect-error brokey
|
||||||
|
// eslint-disable-next-line react/jsx-no-undef
|
||||||
|
<Fragment key={member._id.user}>
|
||||||
<div
|
<div
|
||||||
key={member._id.user}
|
|
||||||
className={styles.member}
|
className={styles.member}
|
||||||
data-open={selected === member._id.user}
|
data-open={selected === member._id.user}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -81,14 +80,15 @@ export const Members = observer(({ server }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
{selected === member._id.user && (
|
{selected === member._id.user && (
|
||||||
<div
|
<div
|
||||||
key={"drop_" + member._id.user}
|
key={`drop_${member._id.user}`}
|
||||||
className={styles.memberView}>
|
className={styles.memberView}>
|
||||||
<Overline type="subtle">Roles</Overline>
|
<Overline type="subtle">Roles</Overline>
|
||||||
{Object.keys(server.roles ?? {}).map(
|
{Object.keys(server.roles ?? {}).map(
|
||||||
(key) => {
|
(key) => {
|
||||||
let role = server.roles![key];
|
const role = server.roles![key];
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
key={key}
|
||||||
checked={
|
checked={
|
||||||
roles.includes(key) ??
|
roles.includes(key) ??
|
||||||
false
|
false
|
||||||
|
@ -134,7 +134,7 @@ export const Members = observer(({ server }: Props) => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,12 +4,11 @@ import { Server } from "revolt.js/dist/maps/Servers";
|
||||||
|
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||||
|
|
||||||
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
||||||
import { AppContext, useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
import { getChannelName } from "../../../context/revoltjs/util";
|
import { getChannelName } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
import Button from "../../../components/ui/Button";
|
import Button from "../../../components/ui/Button";
|
||||||
|
@ -21,8 +20,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Overview = observer(({ server }: Props) => {
|
export const Overview = observer(({ server }: Props) => {
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
const [name, setName] = useState(server.name);
|
const [name, setName] = useState(server.name);
|
||||||
const [description, setDescription] = useState(server.description ?? "");
|
const [description, setDescription] = useState(server.description ?? "");
|
||||||
const [systemMessages, setSystemMessages] = useState(
|
const [systemMessages, setSystemMessages] = useState(
|
||||||
|
@ -41,7 +38,7 @@ export const Overview = observer(({ server }: Props) => {
|
||||||
|
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
function save() {
|
function save() {
|
||||||
const changes: Record<string, any> = {};
|
const changes: Record<string, unknown> = {};
|
||||||
if (name !== server.name) changes.name = name;
|
if (name !== server.name) changes.name = name;
|
||||||
if (description !== server.description)
|
if (description !== server.description)
|
||||||
changes.description = description;
|
changes.description = description;
|
||||||
|
@ -122,6 +119,7 @@ export const Overview = observer(({ server }: Props) => {
|
||||||
].map(([i18n, key]) => (
|
].map(([i18n, key]) => (
|
||||||
// ! FIXME: temporary code just so we can expose the options
|
// ! FIXME: temporary code just so we can expose the options
|
||||||
<p
|
<p
|
||||||
|
key={key}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
|
@ -156,7 +154,7 @@ export const Overview = observer(({ server }: Props) => {
|
||||||
{server.channels
|
{server.channels
|
||||||
.filter((x) => typeof x !== "undefined")
|
.filter((x) => typeof x !== "undefined")
|
||||||
.map((channel) => (
|
.map((channel) => (
|
||||||
<option value={channel!._id}>
|
<option key={channel!._id} value={channel!._id}>
|
||||||
{getChannelName(channel!, true)}
|
{getChannelName(channel!, true)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue