diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..ad0d5488 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: Lounge Chat + url: https://rvlt.gg/Testers + about: Ask questions and discuss with others. + - name: Discussions + url: https://github.com/orgs/revoltchat/discussions + about: For larger feature requests and general question & answer. diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 543a190c..91ee31a7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,7 +5,7 @@ on: branches: - "master" tags: - - "v*" + - "*" paths-ignore: - ".github/**" - "!.github/workflows/docker.yml" diff --git a/.github/workflows/mirroring.yml b/.github/workflows/mirroring.yml index d196a290..8a2cfdc1 100644 --- a/.github/workflows/mirroring.yml +++ b/.github/workflows/mirroring.yml @@ -4,7 +4,6 @@ on: push: branches: - "master" - - "production" jobs: to_gitlab: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index dd47fe91..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,40 +0,0 @@ -image: node:16-buster - -variables: - GIT_SUBMODULE_STRATEGY: recursive - -cache: - paths: - - node_modules - -# Fetch dependencies and setup project for compilation. -install: - stage: prepare - script: - - yarn - -# Type check the project -typecheck: - stage: test - needs: - - install - dependencies: - - install - script: - - yarn typecheck - -# Lint the project and check prettier output. -lint: - stage: test - allow_failure: true - needs: - - install - dependencies: - - install - script: - - yarn lint - - yarn --check 'src/**/*.{js,jsx,ts,tsx}' - -stages: - - prepare - - test diff --git a/.prettierrc.js b/.prettierrc.js index d9170ab2..e3019dc2 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -4,10 +4,12 @@ module.exports = { jsxBracketSameLine: true, importOrder: [ "preact|classnames|.scss$", + "^@revoltchat", "/(lib)", "/(redux|mobx)", "/(context)", - "/(ui|common)|.svg|.webp|.png|.jpg$", + "/(ui|common)$", + ".svg|.webp|.png|.jpg$", "^[./]", ], importOrderSeparation: true, diff --git a/README.md b/README.md index 56fd5836..49d4ac3b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat). +## Pending Rewrite + +The following code is pending a partial or full rewrite: + +- `src/components`: components are being migrated to [revoltchat/components](https://github.com/revoltchat/components) +- `src/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components) +- `src/lib`: this needs to be organised + ## Stack - [Preact](https://preactjs.com/) @@ -42,17 +50,18 @@ You can now access the client at http://local.revolt.chat:3000. ## CLI Commands -| Command | Description | -| ------------------- | -------------------------------------------- | -| `yarn pull` | Setup assets required for Revite. | -| `yarn dev` | Start the Revolt client in development mode. | -| `yarn build` | Build the Revolt client. | -| `yarn preview` | Start a local server with the built client. | -| `yarn lint` | Run ESLint on the client. | -| `yarn fmt` | Run Prettier on the client. | -| `yarn typecheck` | Run TypeScript type checking on the client. | -| `yarn start` | Start a local sirv server with built client. | -| `yarn start:inject` | Inject a given API URL and start server. | +| Command | Description | +| --------------------------------------- | -------------------------------------------- | +| `yarn pull` | Setup assets required for Revite. | +| `yarn dev` | Start the Revolt client in development mode. | +| `yarn build` | Build the Revolt client. | +| `yarn preview` | Start a local server with the built client. | +| `yarn lint` | Run ESLint on the client. | +| `yarn fmt` | Run Prettier on the client. | +| `yarn typecheck` | Run TypeScript type checking on the client. | +| `yarn start` | Start a local sirv server with built client. | +| `yarn start:inject` | Inject a given API URL and start server. | +| `yarn lint \| egrep "no-literals" -B 1` | Scan for untranslated strings. | ## License diff --git a/VERSION b/VERSION deleted file mode 100644 index 2aeddead..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.5.3-1 \ No newline at end of file diff --git a/external/lang b/external/lang index bac88cff..5af7326c 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit bac88cffd196a2afacf7d726e4f7ef19bd6bd94c +Subproject commit 5af7326c286f729ac6dd4cabff9dfdf7c480b631 diff --git a/package.json b/package.json index d858deb2..ba735b78 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.0", + "version": "1.0.1", "scripts": { "dev": "node scripts/setup_assets.js --check && vite", "pull": "node scripts/setup_assets.js", @@ -39,38 +39,19 @@ "varsIgnorePattern": "^_" } ], - "require-jsdoc": [ - "error", - { - "require": { - "FunctionDeclaration": true, - "MethodDefinition": true, - "ClassDeclaration": true, - "ArrowFunctionExpression": false, - "FunctionExpression": false - }, - "ignore": { - "MethodDefinition": [ - "toJSON", - "hydrate" - ] - } - } - ] + "react/jsx-no-literals": "warn" } }, "dependencies": { - "@fontsource/bitter": "^4.5.0", - "@insertish/vite-plugin-babel-macros": "^1.0.5", "fs-extra": "^10.0.0", "klaw": "^3.0.0", - "react-beautiful-dnd": "^13.1.0", "sirv-cli": "^1.0.14", "vite": "^2.6.14" }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.17.9", "@fontsource/atkinson-hyperlegible": "^4.4.5", + "@fontsource/bitter": "^4.5.7", "@fontsource/comic-neue": "^4.4.5", "@fontsource/fira-code": "^4.4.5", "@fontsource/inter": "^4.4.5", @@ -90,27 +71,29 @@ "@fontsource/ubuntu": "^4.4.5", "@fontsource/ubuntu-mono": "^4.4.5", "@hcaptcha/react-hcaptcha": "^0.3.6", + "@insertish/vite-plugin-babel-macros": "^1.0.5", "@preact/preset-vite": "^2.0.0", - "@revoltchat/ui": "1.0.31", + "@revoltchat/ui": "1.0.76", "@rollup/plugin-replace": "^2.4.2", "@styled-icons/boxicons-logos": "^10.38.0", "@styled-icons/boxicons-regular": "^10.38.0", "@styled-icons/boxicons-solid": "^10.38.0", "@styled-icons/simple-icons": "^10.33.0", - "@tippyjs/react": "^4.2.5", + "@tippyjs/react": "4.2.6", "@traptitech/markdown-it-katex": "^3.4.3", "@traptitech/markdown-it-spoiler": "^1.1.6", "@trivago/prettier-plugin-sort-imports": "^2.0.2", + "@types/lodash": "^4", "@types/lodash.defaultsdeep": "^4.6.6", "@types/lodash.isequal": "^4.5.5", - "@types/markdown-it": "^12.0.2", "@types/node": "^15.12.4", "@types/preact-i18n": "^2.3.0", "@types/prismjs": "^1.16.5", - "@types/react-beautiful-dnd": "^13.1.2", + "@types/react-beautiful-dnd": "^13", "@types/react-helmet": "^6.1.1", "@types/react-router-dom": "^5.1.7", "@types/react-scroll": "^1.8.2", + "@types/semver": "^7", "@types/styled-components": "^5.1.10", "@types/twemoji": "^12.1.1", "@typescript-eslint/eslint-plugin": "^4.27.0", @@ -122,37 +105,56 @@ "detect-browser": "^5.2.0", "eslint": "^7.28.0", "eslint-config-preact": "^1.1.4", + "eslint-plugin-jsdoc": "^39.3.2", + "eslint-plugin-mobx": "^0.0.8", "eventemitter3": "^4.0.7", + "history": "4", "json-stringify-deterministic": "^1.0.2", "localforage": "^1.9.0", + "lodash": "^4.17.21", "lodash.defaultsdeep": "^4.6.1", "lodash.isequal": "^4.5.0", "long": "^5.2.0", - "markdown-it": "^12.0.6", - "markdown-it-emoji": "^2.0.0", + "mdast-util-to-hast": "^12.1.2", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext", - "mobx": "^6.3.2", - "mobx-react-lite": "^3.3.0", + "mobx": "^6.6.0", + "mobx-react-lite": "3.4.0", "preact": "^10.5.14", - "preact-context-menu": "0.4.0-patch.0", + "preact-context-menu": "0.4.1", "preact-i18n": "^2.4.0-preactx", "prettier": "^2.3.1", - "prismjs": "^1.23.0", - "react-device-detect": "^1.17.0", + "prismjs": "^1.28.0", + "qrcode.react": "^3.0.2", + "react-beautiful-dnd": "^13.1.0", + "react-device-detect": "2.2.2", "react-helmet": "^6.1.0", "react-hook-form": "6.3.0", "react-overlapping-panels": "1.2.2", "react-router-dom": "^5.2.0", "react-scroll": "^1.8.2", - "react-virtuoso": "^1.10.4", - "revolt.js": "6.0.0-2", + "react-virtuoso": "^2.12.0", + "rehype-katex": "^6.0.2", + "rehype-prism": "^2.1.3", + "rehype-react": "^7.1.1", + "remark-breaks": "^3.0.2", + "remark-gfm": "^3.0.1", + "remark-math": "^5.1.1", + "remark-parse": "^10.0.1", + "remark-rehype": "^10.1.0", + "revolt.js": "^6.0.6", "rimraf": "^3.0.2", "sass": "^1.35.1", + "semver": "^7.3.7", "shade-blend-color": "^1.0.0", + "slate": "^0.81.1", + "slate-history": "^0.66.0", + "slate-react": "^0.81.0", "stacktrace-js": "^2.0.2", "styled-components": "^5.3.0", "typescript": "^4.4.2", "ulid": "^2.3.0", + "unified": "^10.1.2", + "unist-util-visit": "^4.1.0", "use-resize-observer": "^7.0.0", "vite-plugin-pwa": "^0.11.13", "workbox-precaching": "^6.1.5" diff --git a/scripts/publish.sh b/scripts/publish.sh index 427d025d..b3b957f0 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,7 +1,30 @@ #!/bin/bash -version=$(cat VERSION) +# Build and publish release to production server + +# Remote Server +REMOTE=revolt-de-nrb-1 + +# Remote Directory +REMOTE_DIR=/root/revite + +# Post-install script +POST_INSTALL="pm2 restart revite" + +# Assets +export REVOLT_SAAS=https://github.com/revoltchat/assets + + +# 1. Build Revite +yarn +yarn build + +# 2. Archive built files +tar -czvf build.tar.gz dist + +# 3. Upload built files +scp build.tar.gz $REMOTE:$REMOTE_DIR/build.tar.gz +rm build.tar.gz + +# 4. Apply changes +ssh $REMOTE "cd $REMOTE_DIR; tar -xvzf build.tar.gz; rm build.tar.gz; $POST_INSTALL" - docker build -t revoltchat/client:${version} . && -docker tag revoltchat/client:${version} revoltchat/client:latest && - docker push revoltchat/client:${version} && - docker push revoltchat/client:latest diff --git a/scripts/setup_assets.js b/scripts/setup_assets.js index 200b89d4..503ae2f8 100644 --- a/scripts/setup_assets.js +++ b/scripts/setup_assets.js @@ -3,8 +3,8 @@ const { copy, remove, access } = require("fs-extra"); const { exec: cexec } = require("child_process"); const { resolve } = require("path"); -let target = process.env.REVOLT_SASS; -let branch = process.env.REVOLT_SASS_BRANCH; +let target = process.env.REVOLT_SAAS; +let branch = process.env.REVOLT_SAAS_BRANCH; let DEFAULT_DIRECTORY = "public/assets_default"; let OUT_DIRECTORY = "public/assets"; diff --git a/src/assets/changelogs.ts b/src/assets/changelogs.ts new file mode 100644 index 00000000..88b60f6a --- /dev/null +++ b/src/assets/changelogs.ts @@ -0,0 +1,38 @@ +type Element = + | string + | { + type: "image"; + src: string; + }; + +export interface ChangelogPost { + date: Date; + title: string; + content: Element[]; +} + +export const changelogEntries: Record = { + 1: { + date: new Date("2022-06-12T20:39:16.674Z"), + title: "Secure your account with 2FA", + content: [ + "Two-factor authentication is now available to all users, you can now head over to settings to enable recovery codes and an authenticator app.", + { + type: "image", + src: "https://autumn.revolt.chat/attachments/E21kwmuJGcASgkVLiSIW0wV3ggcaOWjW0TQF7cdFNY/image.png", + }, + "Once enabled, you will be prompted on login.", + { + type: "image", + src: "https://autumn.revolt.chat/attachments/LWRYoKR2tE1ggW_Lzm547P1pnrkNgmBaoCAfWvHE74/image.png", + }, + "Other authentication methods coming later, stay tuned!", + ], + }, +}; + +export const changelogEntryArray = Object.keys(changelogEntries).map( + (index) => changelogEntries[index as unknown as number], +); + +export const latestChangelog = changelogEntryArray.length; diff --git a/src/assets/emojis.ts b/src/assets/emojis.ts index 7488ac14..0f59412d 100644 --- a/src/assets/emojis.ts +++ b/src/assets/emojis.ts @@ -1850,7 +1850,7 @@ export const emojiDictionary = { scotland: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", wales: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", ...{ - "1984": "custom:1984.gif", + 1984: "custom:1984.gif", KekW: "custom:KekW.png", amogus: "custom:amogus.gif", awaa: "custom:awaa.png", @@ -1952,5 +1952,6 @@ export const emojiDictionary = { huggies: "custom:huggies.png", noted: "custom:noted.gif", waving: "custom:waving.png", + mogusvented: "custom:mogusvented.png", }, }; diff --git a/src/components/README.md b/src/components/README.md new file mode 100644 index 00000000..2aa9f1cc --- /dev/null +++ b/src/components/README.md @@ -0,0 +1,14 @@ +The following folders should not be added to or modified: + +- `common` +- `markdown` +- `native` +- `ui` + +The following are part-legacy, will remain in place and will be rewritten to some degree still: + +- `navigation` + +The following are mostly good to go: + +- `settings` diff --git a/src/components/common/AgeGate.tsx b/src/components/common/AgeGate.tsx index 6641877b..a7bea21e 100644 --- a/src/components/common/AgeGate.tsx +++ b/src/components/common/AgeGate.tsx @@ -6,14 +6,11 @@ import styled from "styled-components/macro"; import { Text } from "preact-i18n"; import { useState } from "preact/hooks"; +import { Button, Checkbox } from "@revoltchat/ui"; + import { useApplicationState } from "../../mobx/State"; import { SECTION_NSFW } from "../../mobx/stores/Layout"; -import Button from "../ui/Button"; -import Checkbox from "../ui/Checkbox"; - -import { Children } from "../../types/Preact"; - const Base = styled.div` display: flex; flex-grow: 1; @@ -80,16 +77,16 @@ export default observer((props: Props) => { layout.toggleSectionState(SECTION_NSFW, false)}> - - + title={} + value={layout.getSectionState(SECTION_NSFW, false)} + onChange={() => layout.toggleSectionState(SECTION_NSFW, false)} + />
- )} - {joinError && } + {joinError && ( + + + + )} ); } diff --git a/src/components/common/messaging/embed/EmbedMedia.tsx b/src/components/common/messaging/embed/EmbedMedia.tsx index f602318b..201b3d84 100644 --- a/src/components/common/messaging/embed/EmbedMedia.tsx +++ b/src/components/common/messaging/embed/EmbedMedia.tsx @@ -3,8 +3,8 @@ import { API } from "revolt.js"; import styles from "./Embed.module.scss"; -import { useIntermediate } from "../../../../context/intermediate/Intermediate"; -import { useClient } from "../../../../context/revoltjs/RevoltClient"; +import { useClient } from "../../../../controllers/client/ClientController"; +import { modalController } from "../../../../controllers/modals/ModalController"; interface Props { embed: API.Embed; @@ -14,7 +14,6 @@ interface Props { export default function EmbedMedia({ embed, width, height }: Props) { if (embed.type !== "Website") return null; - const { openScreen } = useIntermediate(); const client = useClient(); switch (embed.special?.type) { @@ -117,8 +116,8 @@ export default function EmbedMedia({ embed, width, height }: Props) { loading="lazy" style={{ width, height }} onClick={() => - openScreen({ - id: "image_viewer", + modalController.push({ + type: "image_viewer", embed: embed.image!, }) } diff --git a/src/components/common/messaging/embed/EmbedMediaActions.tsx b/src/components/common/messaging/embed/EmbedMediaActions.tsx index 27f82c75..ea0e83fe 100644 --- a/src/components/common/messaging/embed/EmbedMediaActions.tsx +++ b/src/components/common/messaging/embed/EmbedMediaActions.tsx @@ -3,7 +3,7 @@ import { API } from "revolt.js"; import styles from "./Embed.module.scss"; -import IconButton from "../../../ui/IconButton"; +import { IconButton } from "@revoltchat/ui"; interface Props { embed: API.Image; @@ -20,7 +20,7 @@ export default function EmbedMediaActions({ embed }: Props) { diff --git a/src/components/common/user/UserBadges.tsx b/src/components/common/user/UserBadges.tsx index b8c36e44..b14d8c6a 100644 --- a/src/components/common/user/UserBadges.tsx +++ b/src/components/common/user/UserBadges.tsx @@ -16,6 +16,7 @@ enum Badges { Paw = 128, EarlyAdopter = 256, ReservedRelevantJokeBadge1 = 512, + ReservedRelevantJokeBadge2 = 1024, } const BadgesBase = styled.div` @@ -135,6 +136,13 @@ export default function UserBadges({ badges, uid }: Props) { ) : ( <> )} + {badges & Badges.ReservedRelevantJokeBadge2 ? ( + + + + ) : ( + <> + )} {badges & Badges.Paw ? ( diff --git a/src/components/common/user/UserCheckbox.tsx b/src/components/common/user/UserCheckbox.tsx index f810bc43..15ffce7d 100644 --- a/src/components/common/user/UserCheckbox.tsx +++ b/src/components/common/user/UserCheckbox.tsx @@ -1,17 +1,24 @@ import { User } from "revolt.js"; -import Checkbox, { CheckboxProps } from "../../ui/Checkbox"; +import { Checkbox, Row, Column } from "@revoltchat/ui"; import UserIcon from "./UserIcon"; import { Username } from "./UserShort"; -type UserProps = Omit & { user: User }; +type UserProps = { value: boolean; onChange: (v: boolean) => void; user: User }; export default function UserCheckbox({ user, ...props }: UserProps) { return ( - - - - + + + + + + + } + /> ); } diff --git a/src/components/common/user/UserHeader.tsx b/src/components/common/user/UserHeader.tsx index e3d207c6..1c442799 100644 --- a/src/components/common/user/UserHeader.tsx +++ b/src/components/common/user/UserHeader.tsx @@ -7,13 +7,11 @@ import styled from "styled-components/macro"; import { openContextMenu } from "preact-context-menu"; import { Text, Localizer } from "preact-i18n"; +import { Header, IconButton } from "@revoltchat/ui"; + import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - -import Header from "../../ui/Header"; -import IconButton from "../../ui/IconButton"; - +import { modalController } from "../../../controllers/modals/ModalController"; import Tooltip from "../Tooltip"; import UserStatus from "./UserStatus"; @@ -49,16 +47,16 @@ interface Props { } export default observer(({ user }: Props) => { - const { writeClipboard } = useIntermediate(); - return ( -
+
}> writeClipboard(user.username)}> + onClick={() => + modalController.writeText(user.username) + }> @{user.username} diff --git a/src/components/common/user/UserHover.tsx b/src/components/common/user/UserHover.tsx index d04536a3..37909054 100644 --- a/src/components/common/user/UserHover.tsx +++ b/src/components/common/user/UserHover.tsx @@ -1,7 +1,6 @@ import { User } from "revolt.js"; import styled from "styled-components/macro"; -import { Children } from "../../../types/Preact"; import Tooltip from "../Tooltip"; import { Username } from "./UserShort"; import UserStatus from "./UserStatus"; diff --git a/src/components/common/user/UserIcon.tsx b/src/components/common/user/UserIcon.tsx index 46fe1bc3..3c96b9a6 100644 --- a/src/components/common/user/UserIcon.tsx +++ b/src/components/common/user/UserIcon.tsx @@ -6,10 +6,9 @@ import styled, { css } from "styled-components/macro"; import { useApplicationState } from "../../../mobx/State"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - import fallback from "../assets/user.png"; +import { useClient } from "../../../controllers/client/ClientController"; import IconBase, { IconBaseProps } from "../IconBase"; type VoiceStatus = "muted" | "deaf"; @@ -114,7 +113,7 @@ export default observer( y="0" width="32" height="32" - class="icon" + className="icon" mask={mask ?? (status ? "url(#user)" : undefined)}> {} diff --git a/src/components/common/user/UserShort.tsx b/src/components/common/user/UserShort.tsx index 881d763f..bbe3670a 100644 --- a/src/components/common/user/UserShort.tsx +++ b/src/components/common/user/UserShort.tsx @@ -8,9 +8,8 @@ import { Text } from "preact-i18n"; import { internalEmit } from "../../../lib/eventEmitter"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - +import { useClient } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; import UserIcon from "./UserIcon"; const BotBadge = styled.div` @@ -125,9 +124,9 @@ export default function UserShort({ masquerade?: API.Masquerade; showServerIdentity?: boolean; }) { - const { openScreen } = useIntermediate(); const openProfile = () => - user && openScreen({ id: "profile", user_id: user._id }); + user && + modalController.push({ type: "user_profile", user_id: user._id }); const handleUserClick = (e: MouseEvent) => { if (e.shiftKey && user?._id) { diff --git a/src/components/markdown/Markdown.module.scss b/src/components/markdown/Markdown.module.scss deleted file mode 100644 index c107fcfa..00000000 --- a/src/components/markdown/Markdown.module.scss +++ /dev/null @@ -1,218 +0,0 @@ -.markdown { - :global(.emoji) { - object-fit: contain; - - height: 1.25em; - width: 1.25em; - margin: 0 0.05em 0 0.1em; - vertical-align: -0.2em; - } - - &[data-large-emojis="true"] :global(.emoji) { - width: 3rem; - height: 3rem; - margin-bottom: 0; - margin-top: 1px; - margin-right: 2px; - vertical-align: -0.3em; - } - - p, - pre { - margin: 0; - } - - a { - text-decoration: none; - - &[data-type="mention"] { - padding: 0 6px; - flex-shrink: 0; - font-weight: 600; - display: inline-block; - background: var(--secondary-background); - border-radius: calc(var(--border-radius) * 2); - - &:hover { - text-decoration: none; - } - } - - &:hover { - text-decoration: underline; - } - } - - h1, - h2, - h3, - h4, - h5, - h6, - ul, - ol, - blockquote { - margin: 0; - } - - h1, - h2, - h3, - h4, - h5, - h6 { - &:not(:first-child) { - margin-top: 12px; - } - } - - ul, - ol { - list-style-position: inside; - padding-left: 10px; - } - - blockquote { - margin: 2px 0; - padding: 2px 0; - background: var(--hover); - border-radius: var(--border-radius); - border-inline-start: 4px solid var(--tertiary-background); - - > * { - margin: 0 8px; - } - } - - pre { - padding: 1em; - overflow-x: scroll; - border-radius: var(--border-radius); - background: var(--block) !important; - } - - p > code { - padding: 1px 4px; - flex-shrink: 0; - } - - code { - color: white; - font-size: 90%; - background: var(--block); - border-radius: var(--border-radius); - font-family: var(--monospace-font), monospace; - border-radius: 3px; - -webkit-box-decoration-break: clone; - } - - input[type="checkbox"] { - margin-right: 4px; - pointer-events: none; - } - - table { - border-collapse: collapse; - - th, - td { - padding: 6px; - border: 1px solid var(--tertiary-foreground); - } - } - - :global(.katex-block) { - overflow-x: auto; - } - - :global(.spoiler) { - padding: 0 2px; - cursor: pointer; - user-select: none; - color: transparent; - background: #151515; - border-radius: var(--border-radius); - - > * { - opacity: 0; - pointer-events: none; - } - - &:global(.shown) { - cursor: auto; - user-select: all; - color: var(--foreground); - background: var(--secondary-background); - - > * { - opacity: 1; - pointer-events: unset; - } - } - } - - :global(.code) { - font-family: var(--monospace-font), monospace; - - :global(.lang) { - width: fit-content; - padding-bottom: 8px; - - div { - color: #111; - cursor: pointer; - padding: 2px 6px; - font-weight: 600; - user-select: none; - display: inline-block; - background: var(--accent); - - font-size: 10px; - text-transform: uppercase; - box-shadow: 0 2px #787676; - border-radius: calc(var(--border-radius) / 3); - - &:active { - transform: translateY(1px); - box-shadow: 0 1px #787676; - } - } - } - } - - input[type="checkbox"] { - width: 0; - opacity: 0; - pointer-events: none; - } - - label { - pointer-events: none; - } - - input[type="checkbox"] + label:before { - width: 12px; - height: 12px; - content: "a"; - font-size: 10px; - margin-right: 6px; - line-height: 12px; - background: white; - position: relative; - display: inline-block; - border-radius: var(--border-radius); - } - - input[type="checkbox"][checked="true"] + label:before { - content: "✓"; - align-items: center; - display: inline-flex; - justify-content: center; - background: var(--accent); - } - - input[type="checkbox"] + label { - line-height: 12px; - position: relative; - } -} diff --git a/src/components/markdown/Markdown.tsx b/src/components/markdown/Markdown.tsx index 986fbd2d..77c61dc9 100644 --- a/src/components/markdown/Markdown.tsx +++ b/src/components/markdown/Markdown.tsx @@ -1,13 +1,15 @@ import { Suspense, lazy } from "preact/compat"; -const Renderer = lazy(() => import("./Renderer")); +const Renderer = lazy(() => import("./RemarkRenderer")); export interface MarkdownProps { - content?: string | null; + content: string; disallowBigEmoji?: boolean; } export default function Markdown(props: MarkdownProps) { + if (!props.content) return null; + return ( // @ts-expect-error Typings mis-match. diff --git a/src/components/markdown/RemarkRenderer.tsx b/src/components/markdown/RemarkRenderer.tsx new file mode 100644 index 00000000..e3b2f3d6 --- /dev/null +++ b/src/components/markdown/RemarkRenderer.tsx @@ -0,0 +1,220 @@ +import "katex/dist/katex.min.css"; +import rehypeKatex from "rehype-katex"; +import rehypePrism from "rehype-prism"; +import rehypeReact from "rehype-react"; +import remarkBreaks from "remark-breaks"; +import remarkGfm from "remark-gfm"; +import remarkMath from "remark-math"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import styled, { css } from "styled-components"; +import { unified } from "unified"; + +import { createElement } from "preact"; +import { memo } from "preact/compat"; +import { useLayoutEffect, useMemo, useState } from "preact/hooks"; + +import { MarkdownProps } from "./Markdown"; +import { handlers } from "./hast"; +import { RenderCodeblock } from "./plugins/Codeblock"; +import { RenderAnchor } from "./plugins/anchors"; +import { remarkChannels, RenderChannel } from "./plugins/channels"; +import { isOnlyEmoji, remarkEmoji, RenderEmoji } from "./plugins/emoji"; +import { remarkHtmlToText } from "./plugins/htmlToText"; +import { remarkMention, RenderMention } from "./plugins/mentions"; +import { remarkSpoiler, RenderSpoiler } from "./plugins/spoiler"; +import { remarkTimestamps } from "./plugins/timestamps"; +import "./prism"; + +/** + * Null element + */ +const Null: React.FC = () => null; + +/** + * Custom Markdown components + */ +const components = { + emoji: RenderEmoji, + mention: RenderMention, + spoiler: RenderSpoiler, + channel: RenderChannel, + a: RenderAnchor, + p: styled.p` + margin: 0; + + > code { + padding: 1px 4px; + flex-shrink: 0; + } + `, + h1: styled.h1` + margin: 0.2em 0; + `, + h2: styled.h2` + margin: 0.2em 0; + `, + h3: styled.h3` + margin: 0.2em 0; + `, + h4: styled.h4` + margin: 0.2em 0; + `, + h5: styled.h5` + margin: 0.2em 0; + `, + h6: styled.h6` + margin: 0.2em 0; + `, + pre: RenderCodeblock, + code: styled.code` + color: white; + background: var(--block); + + font-size: 90%; + font-family: var(--monospace-font), monospace; + + border-radius: 3px; + box-decoration-break: clone; + `, + table: styled.table` + border-collapse: collapse; + + th, + td { + padding: 6px; + border: 1px solid var(--tertiary-foreground); + } + `, + ul: styled.ul` + list-style-position: inside; + padding-left: 10px; + margin: 0.2em 0; + `, + ol: styled.ol` + list-style-position: inside; + padding-left: 10px; + margin: 0.2em 0; + `, + li: styled.li` + ${(props) => + props.class === "task-list-item" && + css` + list-style-type: none; + `} + `, + blockquote: styled.blockquote` + margin: 2px 0; + padding: 2px 0; + background: var(--hover); + border-radius: var(--border-radius); + border-inline-start: 4px solid var(--tertiary-background); + + > * { + margin: 0 8px; + } + `, + // Block image elements + img: Null, + // Catch literally everything else just in case + video: Null, + figure: Null, + picture: Null, + source: Null, + audio: Null, + script: Null, + style: Null, +}; + +/** + * Unified Markdown renderer + */ +const render = unified() + .use(remarkParse) + .use(remarkBreaks) + .use(remarkGfm) + .use(remarkMath) + .use(remarkSpoiler) + .use(remarkChannels) + .use(remarkTimestamps) + .use(remarkEmoji) + .use(remarkMention) + .use(remarkHtmlToText) + .use(remarkRehype, { + handlers, + }) + .use(rehypeKatex, { + maxSize: 10, + maxExpand: 0, + trust: false, + strict: false, + output: "html", + throwOnError: false, + errorColor: "var(--error)", + }) + .use(rehypePrism) + // @ts-expect-error typings do not + // match between Preact and React + .use(rehypeReact, { + createElement, + Fragment, + components, + }); + +/** + * Markdown parent container + */ +const Container = styled.div<{ largeEmoji: boolean }>` + // Allow scrolling block math + .math-display { + overflow-x: auto; + } + + // Set emoji size + --emoji-size: ${(props) => (props.largeEmoji ? "3em" : "1.25em")}; + + // Underline link hover + a:hover { + text-decoration: underline; + } +`; + +/** + * Regex for matching execessive blockquotes + */ +const RE_QUOTE = /(^(?:>\s){5})[>\s]+(.*$)/gm; + +/** + * Sanitise Markdown input before rendering + * @param content Input string + * @returns Sanitised string + */ +function sanitise(content: string) { + return ( + content + // Strip excessive blockquote indentation + .replace(RE_QUOTE, (_, m0, m1) => m0 + m1) + ); +} + +/** + * Remark renderer component + */ +export default memo(({ content, disallowBigEmoji }: MarkdownProps) => { + const sanitisedContent = useMemo(() => sanitise(content), [content]); + + const [Content, setContent] = useState(null!); + + useLayoutEffect(() => { + render + .process(sanitisedContent) + .then((file) => setContent(file.result)); + }, [sanitisedContent]); + + const largeEmoji = useMemo( + () => !disallowBigEmoji && isOnlyEmoji(content!), + [content, disallowBigEmoji], + ); + + return {Content}; +}); diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx deleted file mode 100644 index 0c9f75f7..00000000 --- a/src/components/markdown/Renderer.tsx +++ /dev/null @@ -1,292 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import MarkdownKatex from "@traptitech/markdown-it-katex"; -import MarkdownSpoilers from "@traptitech/markdown-it-spoiler"; -import "katex/dist/katex.min.css"; -import MarkdownIt from "markdown-it"; -// @ts-expect-error No typings. -import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare"; -import { RE_MENTIONS } from "revolt.js"; - -import styles from "./Markdown.module.scss"; -import { useCallback, useContext } from "preact/hooks"; - -import { internalEmit } from "../../lib/eventEmitter"; -import { determineLink } from "../../lib/links"; - -import { dayjs } from "../../context/Locale"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; - -import { generateEmoji } from "../common/Emoji"; - -import { emojiDictionary } from "../../assets/emojis"; -import { MarkdownProps } from "./Markdown"; -import Prism from "./prism"; - -// TODO: global.d.ts file for defining globals -declare global { - interface Window { - copycode: (element: HTMLDivElement) => void; - } -} - -// Handler for code block copy. -if (typeof window !== "undefined") { - window.copycode = function (element: HTMLDivElement) { - try { - const code = element.parentElement?.parentElement?.children[1]; - if (code) { - navigator.clipboard.writeText(code.textContent?.trim() ?? ""); - } - } catch (e) {} - }; -} - -export const md: MarkdownIt = MarkdownIt({ - breaks: true, - linkify: true, - highlight: (str, lang) => { - const v = Prism.languages[lang]; - if (v) { - const out = Prism.highlight(str, v, lang); - return `
${lang}
${out}
`; - } - - return `
${md.utils.escapeHtml(
-            str,
-        )}
`; - }, -}) - .disable("image") - .use(MarkdownEmoji, { defs: emojiDictionary }) - .use(MarkdownSpoilers) - .use(MarkdownKatex, { - throwOnError: false, - maxExpand: 0, - maxSize: 10, - strict: false, - errorColor: "var(--error)", - }); - -md.linkify.set({ fuzzyLink: false }); - -// TODO: global.d.ts file for defining globals -declare global { - interface Window { - internalHandleURL: (element: HTMLAnchorElement) => void; - } -} - -// Include emojis. -md.renderer.rules.emoji = function (token, idx) { - return generateEmoji(token[idx].content); -}; - -// Force line breaks. -// https://github.com/markdown-it/markdown-it/issues/211#issuecomment-508380611 -const defaultParagraphRenderer = - md.renderer.rules.paragraph_open || - ((tokens, idx, options, env, self) => - self.renderToken(tokens, idx, options)); - -md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) { - let result = ""; - if (idx > 1) { - const inline = tokens[idx - 2]; - const paragraph = tokens[idx]; - if ( - inline.type === "inline" && - inline.map && - inline.map[1] && - paragraph.map && - paragraph.map[0] - ) { - const diff = paragraph.map[0] - inline.map[1]; - if (diff > 0) { - result = "
".repeat(diff); - } - } - } - - return result + defaultParagraphRenderer(tokens, idx, options, env, self); -}; - -const RE_TWEMOJI = /:(\w+):/g; - -// ! FIXME: Move to library -const RE_CHANNELS = /<#([A-z0-9]{26})>/g; - -const RE_TIME = //g; - -export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { - const client = useContext(AppContext); - const { openLink } = useIntermediate(); - - if (typeof content === "undefined") return null; - if (!content || content.length === 0) return null; - - // We replace the message with the mention at the time of render. - // We don't care if the mention changes. - const newContent = content - .replace(RE_TIME, (sub: string, ...args: unknown[]) => { - if (isNaN(args[0] as number)) return sub; - const date = dayjs.unix(args[0] as number); - const format = args[1] as string; - let final = ""; - switch (format) { - case "t": - final = date.format("hh:mm"); - break; - case "T": - final = date.format("hh:mm:ss"); - break; - case "R": - final = date.fromNow(); - break; - case "D": - final = date.format("DD MMMM YYYY"); - break; - case "F": - final = date.format("dddd, DD MMMM YYYY hh:mm"); - break; - default: - final = date.format("DD MMMM YYYY hh:mm"); - break; - } - return `\`${final}\``; - }) - .replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => { - const id = args[0] as string, - user = client.users.get(id); - - if (user) { - return `[@${user.username}](/@${id})`; - } - - return sub; - }) - .replace(RE_CHANNELS, (sub: string, ...args: unknown[]) => { - const id = args[0] as string, - channel = client.channels.get(id); - - if (channel?.channel_type === "TextChannel") { - return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`; - } - - return sub; - }); - - const useLargeEmojis = disallowBigEmoji - ? false - : content.replace(RE_TWEMOJI, "").trim().length === 0; - - const toggle = useCallback((ev: MouseEvent) => { - if (ev.currentTarget) { - const element = ev.currentTarget as HTMLDivElement; - if (element.classList.contains("spoiler")) { - element.classList.add("shown"); - } - } - }, []); - - const handleLink = useCallback( - (ev: MouseEvent) => { - if (ev.currentTarget) { - const element = ev.currentTarget as HTMLAnchorElement; - - if (ev.shiftKey) { - switch (element.dataset.type) { - case "mention": { - internalEmit( - "MessageBox", - "append", - `<@${element.dataset.mentionId}>`, - "mention", - ); - ev.preventDefault(); - return; - } - case "channel_mention": { - internalEmit( - "MessageBox", - "append", - `<#${element.dataset.mentionId}>`, - "channel_mention", - ); - ev.preventDefault(); - return; - } - } - } - - if (openLink(element.href)) { - ev.preventDefault(); - } - } - }, - [openLink], - ); - - return ( - { - if (el) { - el.querySelectorAll(".spoiler").forEach( - (element) => { - element.removeEventListener("click", toggle); - element.addEventListener("click", toggle); - }, - ); - - el.querySelectorAll("a").forEach( - (element) => { - element.removeEventListener("click", handleLink); - element.addEventListener("click", handleLink); - element.removeAttribute("data-type"); - element.removeAttribute("data-mention-id"); - element.removeAttribute("target"); - - const link = determineLink(element.href); - switch (link.type) { - case "profile": { - element.setAttribute( - "data-type", - "mention", - ); - element.setAttribute( - "data-mention-id", - link.id, - ); - break; - } - case "navigate": { - if (link.navigation_type === "channel") { - element.setAttribute( - "data-type", - "channel_mention", - ); - element.setAttribute( - "data-mention-id", - link.channel_id, - ); - } - break; - } - case "external": { - element.setAttribute("target", "_blank"); - element.setAttribute("rel", "noreferrer"); - break; - } - } - }, - ); - } - }} - className={styles.markdown} - dangerouslySetInnerHTML={{ - __html: md.render(newContent), - }} - data-large-emojis={useLargeEmojis} - /> - ); -} diff --git a/src/components/markdown/hast.ts b/src/components/markdown/hast.ts new file mode 100644 index 00000000..d2054aff --- /dev/null +++ b/src/components/markdown/hast.ts @@ -0,0 +1,7 @@ +import { passThroughComponents } from "./plugins/remarkRegexComponent"; +import { timestampHandler } from "./plugins/timestamps"; + +export const handlers = { + ...passThroughComponents("emoji", "spoiler", "mention", "channel"), + timestamp: timestampHandler, +}; diff --git a/src/components/markdown/plugins/Codeblock.tsx b/src/components/markdown/plugins/Codeblock.tsx new file mode 100644 index 00000000..85f27ece --- /dev/null +++ b/src/components/markdown/plugins/Codeblock.tsx @@ -0,0 +1,78 @@ +import styled from "styled-components"; + +import { useCallback, useRef } from "preact/hooks"; + +import { Tooltip } from "@revoltchat/ui"; + +import { modalController } from "../../../controllers/modals/ModalController"; + +/** + * Base codeblock styles + */ +const Base = styled.pre` + padding: 1em; + overflow-x: scroll; + background: var(--block); + border-radius: var(--border-radius); +`; + +/** + * Copy codeblock contents button styles + */ +const Lang = styled.div` + width: fit-content; + padding-bottom: 8px; + + a { + color: #111; + cursor: pointer; + padding: 2px 6px; + font-weight: 600; + user-select: none; + display: inline-block; + background: var(--accent); + + font-size: 10px; + text-transform: uppercase; + box-shadow: 0 2px #787676; + border-radius: calc(var(--border-radius) / 3); + + &:active { + transform: translateY(1px); + box-shadow: 0 1px #787676; + } + } +`; + +/** + * Render a codeblock with copy text button + */ +export const RenderCodeblock: React.FC<{ class: string }> = ({ + children, + ...props +}) => { + const ref = useRef(null); + + let text = "text"; + if (props.class) { + text = props.class.split("-")[1]; + } + + const onCopy = useCallback(() => { + const text = ref.current?.querySelector("code")?.innerText; + text && modalController.writeText(text); + }, [ref]); + + return ( + + + + {/** + // @ts-expect-error Preact-React */} +
{text} + + + {children} + + ); +}; diff --git a/src/components/markdown/plugins/anchors.tsx b/src/components/markdown/plugins/anchors.tsx new file mode 100644 index 00000000..8d616247 --- /dev/null +++ b/src/components/markdown/plugins/anchors.tsx @@ -0,0 +1,34 @@ +import { Link } from "react-router-dom"; + +import { determineLink } from "../../../lib/links"; + +import { modalController } from "../../../controllers/modals/ModalController"; + +export function RenderAnchor({ + href, + ...props +}: JSX.HTMLAttributes) { + // Pass-through no href or if anchor + if (!href || href.startsWith("#")) return ; + + // Determine type of link + const link = determineLink(href); + if (link.type === "none") return ; + + // Render direct link if internal + if (link.type === "navigate") { + return ; + } + + return ( + + modalController.openLink(href) && ev.preventDefault() + } + /> + ); +} diff --git a/src/components/markdown/plugins/channels.tsx b/src/components/markdown/plugins/channels.tsx new file mode 100644 index 00000000..a4654fdc --- /dev/null +++ b/src/components/markdown/plugins/channels.tsx @@ -0,0 +1,21 @@ +import { Link } from "react-router-dom"; + +import { clientController } from "../../../controllers/client/ClientController"; +import { createComponent, CustomComponentProps } from "./remarkRegexComponent"; + +export function RenderChannel({ match }: CustomComponentProps) { + const channel = clientController.getAvailableClient().channels.get(match)!; + + return ( + {`#${channel.name}`} + ); +} + +export const remarkChannels = createComponent( + "channel", + /<#([A-z0-9]{26})>/g, + (match) => clientController.getAvailableClient().channels.has(match), +); diff --git a/src/components/markdown/plugins/emoji.tsx b/src/components/markdown/plugins/emoji.tsx new file mode 100644 index 00000000..15ca52e4 --- /dev/null +++ b/src/components/markdown/plugins/emoji.tsx @@ -0,0 +1,63 @@ +import styled from "styled-components"; + +import { useState } from "preact/hooks"; + +import { emojiDictionary } from "../../../assets/emojis"; +import { clientController } from "../../../controllers/client/ClientController"; +import { parseEmoji } from "../../common/Emoji"; +import { createComponent, CustomComponentProps } from "./remarkRegexComponent"; + +const Emoji = styled.img` + object-fit: contain; + + height: var(--emoji-size); + width: var(--emoji-size); + margin: 0 0.05em 0 0.1em; + vertical-align: -0.2em; + + img:before { + content: " "; + display: block; + position: absolute; + height: 50px; + width: 50px; + background-image: url(ishere.jpg); + } +`; + +export function RenderEmoji({ match }: CustomComponentProps) { + const [fail, setFail] = useState(false); + const url = + match in emojiDictionary + ? parseEmoji(emojiDictionary[match as keyof typeof emojiDictionary]) + : `${ + clientController.getAvailableClient().configuration?.features + .autumn.url + }/emojis/${match}`; + + if (fail) return {`:${match}:`}; + + return ( + setFail(true)} + /> + ); +} + +const RE_EMOJI = /:([a-zA-Z0-9_+]+):/g; +const RE_ULID = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; + +export const remarkEmoji = createComponent( + "emoji", + RE_EMOJI, + (match) => match in emojiDictionary || RE_ULID.test(match), +); + +export function isOnlyEmoji(text: string) { + return text.replaceAll(RE_EMOJI, "").trim().length === 0; +} diff --git a/src/components/markdown/plugins/htmlToText.ts b/src/components/markdown/plugins/htmlToText.ts new file mode 100644 index 00000000..abcbbcdb --- /dev/null +++ b/src/components/markdown/plugins/htmlToText.ts @@ -0,0 +1,10 @@ +import { Plugin } from "unified"; +import { visit } from "unist-util-visit"; + +export const remarkHtmlToText: Plugin = () => { + return (tree) => { + visit(tree, "html", (node: { type: string; value: string }) => { + node.type = "text"; + }); + }; +}; diff --git a/src/components/markdown/plugins/mentions.tsx b/src/components/markdown/plugins/mentions.tsx new file mode 100644 index 00000000..eae6938b --- /dev/null +++ b/src/components/markdown/plugins/mentions.tsx @@ -0,0 +1,53 @@ +import { RE_MENTIONS } from "revolt.js"; +import styled from "styled-components"; + +import { clientController } from "../../../controllers/client/ClientController"; +import UserShort from "../../common/user/UserShort"; +import { createComponent, CustomComponentProps } from "./remarkRegexComponent"; + +const Mention = styled.a` + gap: 4px; + flex-shrink: 0; + padding-left: 2px; + padding-right: 6px; + align-items: center; + display: inline-flex; + vertical-align: middle; + + cursor: pointer; + + font-weight: 600; + text-decoration: none !important; + background: var(--secondary-background); + border-radius: calc(var(--border-radius) * 2); + + transition: 0.1s ease filter; + + &:hover { + filter: brightness(0.75); + } + + &:active { + filter: brightness(0.65); + } + + svg { + width: 1em; + height: 1em; + } +`; + +export function RenderMention({ match }: CustomComponentProps) { + return ( + + + + ); +} + +export const remarkMention = createComponent("mention", RE_MENTIONS, (match) => + clientController.getAvailableClient().users.has(match), +); diff --git a/src/components/markdown/plugins/remarkRegexComponent.ts b/src/components/markdown/plugins/remarkRegexComponent.ts new file mode 100644 index 00000000..6693f7b5 --- /dev/null +++ b/src/components/markdown/plugins/remarkRegexComponent.ts @@ -0,0 +1,108 @@ +import type { Handler } from "mdast-util-to-hast"; +import type { Plugin } from "unified"; +import { visit } from "unist-util-visit"; + +/** + * Props given to custom components + */ +export interface CustomComponentProps { + type: string; + match: string; + arg1: string; +} + +/** + * Create a new custom component matched by a given RegExp + * @param type hast node type + * @param regex Regex to match (must have one capture group) + * @returns Unified Plugin + */ +export function createComponent( + type: string, + regex: RegExp, + validator?: (match: string) => boolean, +): Plugin { + /** + * Plugin which transforms a given RegExp into a custom component with given name. + */ + return () => { + return (tree) => { + visit( + tree, + "text", + ( + node: { value: string }, + index: number, + parent: { children: any[] }, + ) => { + const result = []; + let start = 0; + + regex.lastIndex = 0; + + let match = regex.exec(node.value); + + while (match) { + if (!validator || validator(match[1])) { + const position = match.index; + + if (start !== position) { + result.push({ + type: "text", + value: node.value.slice(start, position), + }); + } + + result.push({ + type, + match: match[1], + arg1: match[2], + }); + start = position + match[0].length; + } + + match = regex.exec(node.value); + } + + if ( + result.length > 0 && + parent && + typeof index === "number" + ) { + if (start < node.value.length) { + result.push({ + type: "text", + value: node.value.slice(start), + }); + } + + parent.children.splice(index, 1, ...result); + return index + result.length; + } + }, + ); + }; + }; +} + +/** + * Pass-through a component as-is from remark to rehype + * @param name Tag name + * @returns Handler + */ +export const passThroughRehype: (name: string) => Handler = + (name: string) => (h, node) => + h(node, name, node); + +/** + * Pass-through multiple components at once + * @param keys Tags + * @returns Handlers + */ +export const passThroughComponents = (...keys: string[]) => { + const obj: Record = {}; + for (const key of keys) { + obj[key] = passThroughRehype(key); + } + return obj; +}; diff --git a/src/components/markdown/plugins/spoiler.tsx b/src/components/markdown/plugins/spoiler.tsx new file mode 100644 index 00000000..1986bde3 --- /dev/null +++ b/src/components/markdown/plugins/spoiler.tsx @@ -0,0 +1,45 @@ +import styled, { css } from "styled-components"; + +import { useState } from "preact/hooks"; + +import { createComponent, CustomComponentProps } from "./remarkRegexComponent"; + +const Spoiler = styled.span<{ shown: boolean }>` + padding: 0 2px; + cursor: pointer; + user-select: none; + color: transparent; + background: #151515; + border-radius: var(--border-radius); + + > * { + opacity: 0; + pointer-events: none; + } + + ${(props) => + props.shown && + css` + cursor: auto; + user-select: all; + color: var(--foreground); + background: var(--secondary-background); + + > * { + opacity: 1; + pointer-events: unset; + } + `} +`; + +export function RenderSpoiler({ match }: CustomComponentProps) { + const [shown, setShown] = useState(false); + + return ( + setShown(true)}> + {match} + + ); +} + +export const remarkSpoiler = createComponent("spoiler", /!!([^!]+)!!/g); diff --git a/src/components/markdown/plugins/timestamps.ts b/src/components/markdown/plugins/timestamps.ts new file mode 100644 index 00000000..a6760b24 --- /dev/null +++ b/src/components/markdown/plugins/timestamps.ts @@ -0,0 +1,39 @@ +import type { Handler } from "mdast-util-to-hast"; + +import { dayjs } from "../../../context/Locale"; + +import { createComponent } from "./remarkRegexComponent"; + +export const timestampHandler: Handler = (h, { match, arg1 }) => { + if (isNaN(match)) return { type: "text", value: match }; + const date = dayjs.unix(match); + + let value = ""; + switch (arg1) { + case "t": + value = date.format("hh:mm"); + break; + case "T": + value = date.format("hh:mm:ss"); + break; + case "R": + value = date.fromNow(); + break; + case "D": + value = date.format("DD MMMM YYYY"); + break; + case "F": + value = date.format("dddd, DD MMMM YYYY hh:mm"); + break; + default: + value = date.format("DD MMMM YYYY hh:mm"); + break; + } + + return h(null, "code", {}, [{ type: "text", value }]); +}; + +export const remarkTimestamps = createComponent( + "timestamp", + //g, +); diff --git a/src/components/markdown/prism.ts b/src/components/markdown/prism.ts index 1ae510c6..b76a9c54 100644 --- a/src/components/markdown/prism.ts +++ b/src/components/markdown/prism.ts @@ -34,7 +34,6 @@ import "prismjs/components/prism-r"; import "prismjs/components/prism-sql"; import "prismjs/components/prism-graphql"; import "prismjs/components/prism-shell-session"; -import "prismjs/components/prism-java"; import "prismjs/components/prism-powershell"; import "prismjs/components/prism-swift"; import "prismjs/components/prism-yaml"; @@ -87,7 +86,6 @@ import "prismjs/components/prism-moonscript"; import "prismjs/components/prism-qml"; import "prismjs/components/prism-vim"; import "prismjs/components/prism-nim"; -import "prismjs/components/prism-swift"; import "prismjs/components/prism-haml"; import "prismjs/components/prism-ada"; import "prismjs/components/prism-arduino"; diff --git a/src/components/native/Titlebar.tsx b/src/components/native/Titlebar.tsx index 665e074d..5b7102e3 100644 --- a/src/components/native/Titlebar.tsx +++ b/src/components/native/Titlebar.tsx @@ -98,7 +98,7 @@ const TitlebarBase = styled.div` export function Titlebar(props: Props) { return ( -
+
@@ -114,7 +114,7 @@ export function Titlebar(props: Props) { )}
- {/*
+ {/*
@@ -130,9 +130,9 @@ export function Titlebar(props: Props) {
*/} -
+
-
+
-
+
` flex: 1; + color: var(--foreground); + + // ok + * { + color: var(--foreground) !important; + } > a, > div, @@ -63,7 +74,7 @@ export default observer(() => { {/**/} diff --git a/src/components/navigation/items/ButtonItem.tsx b/src/components/navigation/items/ButtonItem.tsx index 7d6a8467..8d812039 100644 --- a/src/components/navigation/items/ButtonItem.tsx +++ b/src/components/navigation/items/ButtonItem.tsx @@ -5,23 +5,20 @@ import { User, Channel } from "revolt.js"; import styles from "./Item.module.scss"; import classNames from "classnames"; -import { Ref } from "preact"; import { useTriggerEvents } from "preact-context-menu"; import { Localizer, Text } from "preact-i18n"; +import { IconButton } from "@revoltchat/ui"; + import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { stopPropagation } from "../../../lib/stopPropagation"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - +import { modalController } from "../../../controllers/modals/ModalController"; import ChannelIcon from "../../common/ChannelIcon"; import Tooltip from "../../common/Tooltip"; import UserIcon from "../../common/user/UserIcon"; import { Username } from "../../common/user/UserShort"; import UserStatus from "../../common/user/UserStatus"; -import IconButton from "../../ui/IconButton"; - -import { Children } from "../../../types/Preact"; type CommonProps = Omit< JSX.HTMLAttributes, @@ -52,7 +49,6 @@ export const UserButton = observer((props: UserProps) => { channel, ...divProps } = props; - const { openScreen } = useIntermediate(); return (
{ className={styles.icon} onClick={(e) => stopPropagation(e) && - openScreen({ - id: "special_prompt", + modalController.push({ type: "close_dm", target: channel, }) @@ -151,7 +146,6 @@ export const ChannelButton = observer((props: ChannelProps) => { return ; } - const { openScreen } = useIntermediate(); const alerting = alert && !muted && !active; return ( @@ -166,11 +160,9 @@ export const ChannelButton = observer((props: ChannelProps) => { channel: channel._id, unread: !!alert, })}> - +
+ +
{channel.name}
{channel.channel_type === "Group" && ( @@ -199,8 +191,7 @@ export const ChannelButton = observer((props: ChannelProps) => { - openScreen({ - id: "special_prompt", + modalController.push({ type: "leave_group", target: channel, }) diff --git a/src/components/navigation/items/ConnectionStatus.tsx b/src/components/navigation/items/ConnectionStatus.tsx index de0d42b7..46a9f6cc 100644 --- a/src/components/navigation/items/ConnectionStatus.tsx +++ b/src/components/navigation/items/ConnectionStatus.tsx @@ -1,45 +1,47 @@ +import { observer } from "mobx-react-lite"; + import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; -import { - ClientStatus, - StatusContext, - useClient, -} from "../../../context/revoltjs/RevoltClient"; +import { Banner, Button, Column } from "@revoltchat/ui"; -import Banner from "../../ui/Banner"; +import { useSession } from "../../../controllers/client/ClientController"; -export default function ConnectionStatus() { - const status = useContext(StatusContext); - const client = useClient(); +function ConnectionStatus() { + const session = useSession()!; - if (status === ClientStatus.OFFLINE) { + if (session.state === "Offline") { return ( ); - } else if (status === ClientStatus.DISCONNECTED) { + } else if (session.state === "Disconnected") { return ( -
- client.websocket.connect()}> - - + + + +
); - } else if (status === ClientStatus.CONNECTING) { - return ( - - - - ); - } else if (status === ClientStatus.RECONNECTING) { + } else if (session.state === "Connecting") { return ( ); } + return null; } + +export default observer(ConnectionStatus); diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx index bb471340..e61439f9 100644 --- a/src/components/navigation/left/HomeSidebar.tsx +++ b/src/components/navigation/left/HomeSidebar.tsx @@ -1,3 +1,4 @@ +import { Plus } from "@styled-icons/boxicons-regular"; import { Home, UserDetail, @@ -11,18 +12,18 @@ import styled, { css } from "styled-components/macro"; import { Text } from "preact-i18n"; import { useContext, useEffect } from "preact/hooks"; +import { Category, IconButton } from "@revoltchat/ui"; + import ConditionalLink from "../../../lib/ConditionalLink"; import PaintCounter from "../../../lib/PaintCounter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../../mobx/State"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { AppContext } from "../../../context/revoltjs/RevoltClient"; - -import Category from "../../ui/Category"; import placeholderSVG from "../items/placeholder.svg"; +import { useClient } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; import ButtonItem, { ChannelButton } from "../items/ButtonItem"; import ConnectionStatus from "../items/ConnectionStatus"; @@ -44,10 +45,9 @@ const Navbar = styled.div` export default observer(() => { const { pathname } = useLocation(); - const client = useContext(AppContext); + const client = useClient(); const state = useApplicationState(); const { channel: channel_id } = useParams<{ channel: string }>(); - const { openScreen } = useIntermediate(); const channels = [...client.channels.values()].filter( (x) => @@ -125,15 +125,17 @@ export default observer(() => { )} - } - action={() => - openScreen({ - id: "special_input", - type: "create_group", - }) - } - /> + + + + modalController.push({ + type: "create_group", + }) + }> + + + {channels.length === 0 && ( )} diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx index 2196b16d..b9189b23 100644 --- a/src/components/navigation/left/ServerListSidebar.tsx +++ b/src/components/navigation/left/ServerListSidebar.tsx @@ -1,448 +1,40 @@ -import { Plus } from "@styled-icons/boxicons-regular"; -import { Cog, Compass } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; -import { Link, useHistory, useLocation, useParams } from "react-router-dom"; -import styled, { css } from "styled-components/macro"; +import { useParams } from "react-router-dom"; -import { useTriggerEvents } from "preact-context-menu"; +import { useCallback } from "preact/hooks"; -import ConditionalLink from "../../../lib/ConditionalLink"; -import PaintCounter from "../../../lib/PaintCounter"; -import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; +import { ServerList } from "@revoltchat/ui"; import { useApplicationState } from "../../../mobx/State"; -import { SIDEBAR_CHANNELS } from "../../../mobx/stores/Layout"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - -import ChannelIcon from "../../common/ChannelIcon"; -import ServerIcon from "../../common/ServerIcon"; -import Tooltip from "../../common/Tooltip"; -import UserHover from "../../common/user/UserHover"; -import UserIcon from "../../common/user/UserIcon"; -import IconButton from "../../ui/IconButton"; -import LineDivider from "../../ui/LineDivider"; - -import { Children } from "../../../types/Preact"; - -function Icon({ - children, - unread, - count, - size, -}: { - children: Children; - unread?: "mention" | "unread"; - count: number | 0; - size: number; -}) { - return ( - - ); -} - -const ServersBase = styled.div` - width: 58px; - height: 100%; - padding-inline-start: 2px; - - display: flex; - flex-shrink: 0; - flex-direction: column; - - ${isTouchscreenDevice && - css` - padding-bottom: 50px; - `} -`; - -const ServerList = styled.div` - flex-grow: 1; - display: flex; - overflow-y: scroll; - padding-bottom: 20px; - flex-direction: column; - - scrollbar-width: none; - - > :first-child > svg { - margin: 6px 0 6px 4px; - } - - &::-webkit-scrollbar { - width: 0px; - } -`; - -const ServerEntry = styled.div<{ active: boolean; home?: boolean }>` - height: 54px; - display: flex; - align-items: center; - - //transition: 0.2s ease height; - - :focus { - outline: 3px solid blue; - } - - > div { - height: 42px; - padding-inline-start: 6px; - - display: grid; - place-items: center; - - border-start-start-radius: 50%; - border-end-start-radius: 50%; - - &:active { - transform: translateY(1px); - } - - ${(props) => - props.active && - css` - &:active { - transform: none; - } - `} - } - - > span { - width: 0; - display: relative; - - ${(props) => - !props.active && - css` - display: none; - `} - - svg { - margin-top: 5px; - pointer-events: none; - } - } - - ${(props) => - (!props.active || props.home) && - css` - cursor: pointer; - `} -`; - -const ServerCircle = styled.div` - width: 54px; - height: 54px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - - .circle { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--primary-background); - border-radius: 50%; - height: 42px; - width: 42px; - transition: background-color 0.1s ease-in; - cursor: pointer; - - > div svg { - color: var(--accent); - } - - &:active { - transform: translateY(1px); - } - } -`; - -const SettingsButton = styled.div` - width: 50px; - height: 56px; - display: grid; - place-items: center; -`; - -function Swoosh() { - const sidebarOpen = useApplicationState().layout.getSectionState( - SIDEBAR_CHANNELS, - true, - ); - const fill = sidebarOpen - ? "var(--sidebar-active)" - : "var(--primary-background)"; - - return ( - - - - - - - - - ); -} +import { useClient } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; +/** + * Server list sidebar shim component + */ export default observer(() => { const client = useClient(); const state = useApplicationState(); - const { server: server_id } = useParams<{ server?: string }>(); - const server = server_id ? client.servers.get(server_id) : undefined; - const servers = [...client.servers.values()]; - const channels = [...client.channels.values()]; - const history = useHistory(); - const path = useLocation().pathname; - const { openScreen } = useIntermediate(); - - let alertCount = [...client.users.values()].filter( - (x) => x.relationship === "Incoming", - ).length; - - const homeActive = - typeof server === "undefined" && - !path.startsWith("/invite") && - !path.startsWith("/discover"); + const createServer = useCallback( + () => + modalController.push({ + type: "create_server", + }), + [], + ); return ( - - - - - -
- homeActive && history.push("/settings") - }> - - 0 ? "mention" : undefined - } - count={alertCount}> - - - -
-
-
- {channels - .filter( - (x) => - ((x.channel_type === "DirectMessage" && x.active) || - x.channel_type === "Group") && - x.unread, - ) - .map((x) => { - const unreadCount = x.mentions.length; - return ( - - -
- 0 - ? "mention" - : "unread" - } - count={unreadCount}> - {x.channel_type === - "DirectMessage" ? ( - - ) : ( - - )} - -
-
- - ); - })} - - {servers.map((server) => { - const active = server._id === server_id; - - const isUnread = server.isUnread(state.notifications); - const mentionCount = server.getMentions( - state.notifications, - ).length; - - return ( - - - - - 0 - ? "mention" - : isUnread - ? "unread" - : undefined - } - count={mentionCount}> - - - - - - ); - })} - {/**/} - - -
- - openScreen({ - id: "special_input", - type: "create_server", - }) - }> - - -
-
-
- {!isTouchscreenDevice && ( - - -
Discover Revolt
-
- NEW -
-
- } - placement="right"> -
- - - )} - - - {!isTouchscreenDevice && ( - - - -
- - - -
- -
-
- )} - - + ); }); diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx index 0e228fa4..b2b602ff 100644 --- a/src/components/navigation/left/ServerSidebar.tsx +++ b/src/components/navigation/left/ServerSidebar.tsx @@ -1,12 +1,12 @@ import { observer } from "mobx-react-lite"; import { Redirect, useParams } from "react-router"; -import { Server } from "revolt.js"; import styled, { css } from "styled-components/macro"; -import { Ref } from "preact"; import { useTriggerEvents } from "preact-context-menu"; import { useEffect } from "preact/hooks"; +import { Category } from "@revoltchat/ui"; + import ConditionalLink from "../../../lib/ConditionalLink"; import PaintCounter from "../../../lib/PaintCounter"; import { internalEmit } from "../../../lib/eventEmitter"; @@ -14,12 +14,9 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../../mobx/State"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - +import { useClient } from "../../../controllers/client/ClientController"; import CollapsibleSection from "../../common/CollapsibleSection"; import ServerHeader from "../../common/ServerHeader"; -import Category from "../../ui/Category"; - import { ChannelButton } from "../items/ButtonItem"; import ConnectionStatus from "../items/ConnectionStatus"; @@ -126,7 +123,7 @@ export default observer(() => { }> + summary={{category.title}}> {channels} , ); diff --git a/src/components/navigation/right/MemberList.tsx b/src/components/navigation/right/MemberList.tsx index 45aef23b..1163fea5 100644 --- a/src/components/navigation/right/MemberList.tsx +++ b/src/components/navigation/right/MemberList.tsx @@ -8,11 +8,7 @@ import { memo } from "preact/compat"; import { internalEmit } from "../../../lib/eventEmitter"; -import { - Screen, - useIntermediate, -} from "../../../context/intermediate/Intermediate"; - +import { modalController } from "../../../controllers/modals/ModalController"; import { UserButton } from "../items/ButtonItem"; export type MemberListGroup = { @@ -55,15 +51,7 @@ const NoOomfie = styled.div` `; const ItemContent = memo( - ({ - item, - context, - openScreen, - }: { - item: User; - context: Channel; - openScreen: (screen: Screen) => void; - }) => ( + ({ item, context }: { item: User; context: Channel }) => ( `, "mention", ); - } else - [ - openScreen({ - id: "profile", - user_id: item._id, - }), - ]; + } else { + modalController.push({ + type: "user_profile", + user_id: item._id, + }); + } }} /> ), @@ -96,8 +83,6 @@ export default function MemberList({ entries: MemberListGroup[]; context: Channel; }) { - const { openScreen } = useIntermediate(); - return ( x.users.length)} @@ -114,7 +99,7 @@ export default function MemberList({ )} {entry.type !== "no_offline" && ( <> - {" - "} + {" – "} {entry.users.length} )} @@ -137,7 +122,8 @@ export default function MemberList({ server, see issue{" "} + target="_blank" + rel="noreferrer"> #128 {" "} for when this will be resolved. @@ -158,11 +144,7 @@ export default function MemberList({ return (
- +
); }} diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx index 364f4ef3..88e71b95 100644 --- a/src/components/navigation/right/MemberSidebar.tsx +++ b/src/components/navigation/right/MemberSidebar.tsx @@ -4,14 +4,12 @@ import { observer } from "mobx-react-lite"; import { useParams } from "react-router-dom"; import { Channel, Server, User, API } from "revolt.js"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useLayoutEffect, useState } from "preact/hooks"; import { - ClientStatus, - StatusContext, + useSession, useClient, -} from "../../../context/revoltjs/RevoltClient"; - +} from "../../../controllers/client/ClientController"; import { GenericSidebarBase } from "../SidebarBase"; import MemberList, { MemberListGroup } from "./MemberList"; @@ -182,7 +180,7 @@ export const GroupMemberSidebar = observer( ); // ! FIXME: this is temporary code until we get lazy guilds like subscriptions -const FETCHED: Set = new Set(); +const FETCHED: Set = new Set(); export function resetMemberSidebarFetched() { FETCHED.clear(); @@ -205,18 +203,18 @@ function shouldSkipOffline(id: string) { export const ServerMemberSidebar = observer( ({ channel }: { channel: Channel }) => { - const client = useClient(); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; useEffect(() => { const server_id = channel.server_id!; - if (status === ClientStatus.ONLINE && !FETCHED.has(server_id)) { + if (session.state === "Online" && !FETCHED.has(server_id)) { FETCHED.add(server_id); channel .server!.syncMembers(shouldSkipOffline(server_id)) .catch(() => FETCHED.delete(server_id)); } - }, [status, channel]); + }, [session.state, channel]); const entries = useEntries( channel, diff --git a/src/components/navigation/right/Search.tsx b/src/components/navigation/right/Search.tsx index 97891c30..f12cb7d2 100644 --- a/src/components/navigation/right/Search.tsx +++ b/src/components/navigation/right/Search.tsx @@ -5,14 +5,10 @@ import styled from "styled-components/macro"; import { Text } from "preact-i18n"; import { useEffect, useState } from "preact/hooks"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; +import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui"; +import { useClient } from "../../../controllers/client/ClientController"; import Message from "../../common/messaging/Message"; -import Button from "../../ui/Button"; -import InputBox from "../../ui/InputBox"; -import Overline from "../../ui/Overline"; -import Preloader from "../../ui/Preloader"; - import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; type SearchState = @@ -102,23 +98,25 @@ export function SearchSidebar({ close }: Props) { - - « back to members - - + + « back to members} + /> + + - + e.key === "Enter" && search()} onChange={(e) => setQuery(e.currentTarget.value)} /> -
+
{["Latest", "Oldest", "Relevance"].map((key) => (
writeClipboard(JSON.stringify(theme))}> + className="code" + onClick={() => + modalController.writeText(JSON.stringify(theme)) + }> }> {" "} {JSON.stringify(theme)} @@ -61,23 +65,15 @@ export default function ThemeTools() { }> - {currentNickname !== "" && ( - - )} -
-
-
- - ); -}); diff --git a/src/context/intermediate/popovers/UserPicker.module.scss b/src/context/intermediate/popovers/UserPicker.module.scss deleted file mode 100644 index 610de5a7..00000000 --- a/src/context/intermediate/popovers/UserPicker.module.scss +++ /dev/null @@ -1,20 +0,0 @@ -.list { - width: 400px; - max-width: 100%; - max-height: 360px; - overflow-y: scroll; - - > label { - > span { - align-items: flex-start !important; - > span { - display: flex; - padding: 4px; - flex-direction: row; - gap: 10px; - justify-content: flex-start; - align-items: center; - } - } - } -} diff --git a/src/context/revoltjs/CheckAuth.tsx b/src/context/revoltjs/CheckAuth.tsx deleted file mode 100644 index 3d76ae92..00000000 --- a/src/context/revoltjs/CheckAuth.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Redirect } from "react-router-dom"; - -import { useApplicationState } from "../../mobx/State"; - -import { Children } from "../../types/Preact"; -import { useClient } from "./RevoltClient"; - -interface Props { - auth?: boolean; - blockRender?: boolean; - - children: Children; -} - -export const CheckAuth = (props: Props) => { - const auth = useApplicationState().auth; - const client = useClient(); - const ready = auth.isLoggedIn() && !!client?.user; - - if (props.auth && !ready) { - if (props.blockRender) return null; - return ; - } else if (!props.auth && ready) { - if (props.blockRender) return null; - return ; - } - - return <>{props.children}; -}; diff --git a/src/context/revoltjs/Notifications.tsx b/src/context/revoltjs/Notifications.tsx deleted file mode 100644 index 48178c63..00000000 --- a/src/context/revoltjs/Notifications.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { Route, Switch, useHistory, useParams } from "react-router-dom"; -import { Message, User } from "revolt.js"; -import { decodeTime } from "ulid"; - -import { useCallback, useContext, useEffect } from "preact/hooks"; - -import { useTranslation } from "../../lib/i18n"; - -import { useApplicationState } from "../../mobx/State"; - -import { AppContext } from "./RevoltClient"; - -const notifications: { [key: string]: Notification } = {}; - -async function createNotification( - title: string, - options: globalThis.NotificationOptions, -) { - try { - return new Notification(title, options); - } catch (err) { - const sw = await navigator.serviceWorker.getRegistration(); - sw?.showNotification(title, options); - } -} - -function Notifier() { - const translate = useTranslation(); - const state = useApplicationState(); - const notifs = state.notifications; - const showNotification = state.settings.get("notifications:desktop"); - - const client = useContext(AppContext); - const { guild: guild_id, channel: channel_id } = useParams<{ - guild: string; - channel: string; - }>(); - const history = useHistory(); - - const message = useCallback( - async (msg: Message) => { - if (msg.channel_id === channel_id && document.hasFocus()) return; - if (!notifs.shouldNotify(msg)) return; - - state.settings.sounds.playSound("message"); - if (!showNotification) return; - - const effectiveName = msg.masquerade?.name ?? msg.author?.username; - - let title; - switch (msg.channel?.channel_type) { - case "SavedMessages": - return; - case "DirectMessage": - title = `@${effectiveName}`; - break; - case "Group": - if (msg.author?._id === "00000000000000000000000000") { - title = msg.channel.name; - } else { - title = `@${effectiveName} - ${msg.channel.name}`; - } - break; - case "TextChannel": - title = `@${effectiveName} (#${msg.channel.name}, ${msg.channel.server?.name})`; - break; - default: - title = msg.channel?._id; - break; - } - - let image; - if (msg.attachments) { - const imageAttachment = msg.attachments.find( - (x) => x.metadata.type === "Image", - ); - if (imageAttachment) { - image = client.generateFileURL(imageAttachment, { - max_side: 720, - }); - } - } - - let body, icon; - if (msg.content) { - body = client.markdownToText(msg.content); - - if (msg.masquerade?.avatar) { - icon = client.proxyFile(msg.masquerade.avatar); - } else { - icon = msg.author?.generateAvatarURL({ max_side: 256 }); - } - } else if (msg.system) { - const users = client.users; - - switch (msg.system.type) { - case "user_added": - case "user_remove": - { - const user = users.get(msg.system.id); - body = translate( - `app.main.channel.system.${ - msg.system.type === "user_added" - ? "added_by" - : "removed_by" - }`, - { - user: user?.username, - other_user: users.get(msg.system.by) - ?.username, - }, - ); - icon = user?.generateAvatarURL({ - max_side: 256, - }); - } - break; - case "user_joined": - case "user_left": - case "user_kicked": - case "user_banned": - { - const user = users.get(msg.system.id); - body = translate( - `app.main.channel.system.${msg.system.type}`, - { user: user?.username }, - ); - icon = user?.generateAvatarURL({ - max_side: 256, - }); - } - break; - case "channel_renamed": - { - const user = users.get(msg.system.by); - body = translate( - `app.main.channel.system.channel_renamed`, - { - user: users.get(msg.system.by)?.username, - name: msg.system.name, - }, - ); - icon = user?.generateAvatarURL({ - max_side: 256, - }); - } - break; - case "channel_description_changed": - case "channel_icon_changed": - { - const user = users.get(msg.system.by); - body = translate( - `app.main.channel.system.${msg.system.type}`, - { user: users.get(msg.system.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 { - history.push(`/channel/${id}`); - } - } - } - }); - - notifications[msg.channel_id] = notif; - notif.addEventListener( - "close", - () => delete notifications[msg.channel_id], - ); - } - }, - [ - history, - showNotification, - translate, - channel_id, - client, - notifs, - state, - ], - ); - - const relationship = useCallback( - async (user: User) => { - if (client.user?.status?.presence === "Busy") return; - if (!showNotification) return; - - let event; - switch (user.relationship) { - case "Incoming": - event = translate("notifications.sent_request", { - person: user.username, - }); - break; - case "Friend": - event = translate("notifications.now_friends", { - person: user.username, - }); - break; - default: - return; - } - - const notif = await createNotification(event, { - icon: user.generateAvatarURL({ max_side: 256 }), - badge: "/assets/icons/android-chrome-512x512.png", - timestamp: +new Date(), - }); - - notif?.addEventListener("click", () => { - history.push(`/friends`); - }); - }, - [client.user?.status?.presence, history, showNotification, translate], - ); - - useEffect(() => { - client.addListener("message", message); - client.addListener("user/relationship", relationship); - - return () => { - client.removeListener("message", message); - client.removeListener("user/relationship", relationship); - }; - }, [ - client, - state, - guild_id, - channel_id, - showNotification, - notifs, - message, - relationship, - ]); - - useEffect(() => { - function visChange() { - if (document.visibilityState === "visible") { - if (notifications[channel_id]) { - notifications[channel_id].close(); - } - } - } - - visChange(); - - document.addEventListener("visibilitychange", visChange); - return () => - document.removeEventListener("visibilitychange", visChange); - }, [guild_id, channel_id]); - - return null; -} - -export default function NotificationsComponent() { - return ( - - - - - - - - - - - - ); -} diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx deleted file mode 100644 index b4fefcce..00000000 --- a/src/context/revoltjs/RevoltClient.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { observer } from "mobx-react-lite"; -import { Client } from "revolt.js"; - -import { createContext } from "preact"; -import { useContext, useEffect, useMemo, useState } from "preact/hooks"; - -import { useApplicationState } from "../../mobx/State"; - -import Preloader from "../../components/ui/Preloader"; - -import { Children } from "../../types/Preact"; -import { useIntermediate } from "../intermediate/Intermediate"; -import { registerEvents } from "./events"; -import { takeError } from "./util"; - -export enum ClientStatus { - READY, - LOADING, - OFFLINE, - DISCONNECTED, - CONNECTING, - RECONNECTING, - ONLINE, -} - -export interface ClientOperations { - logout: (shouldRequest?: boolean) => Promise; -} - -export const AppContext = createContext(null!); -export const StatusContext = createContext(null!); -export const LogOutContext = createContext(() => {}); - -type Props = { - children: Children; -}; - -export default observer(({ children }: Props) => { - const state = useApplicationState(); - const { openScreen } = useIntermediate(); - const [client, setClient] = useState(null!); - const [status, setStatus] = useState(ClientStatus.LOADING); - const [loaded, setLoaded] = useState(false); - - function logout() { - setLoaded(false); - client.logout(false); - } - - useEffect(() => { - if (navigator.onLine) { - state.config.createClient().api.get("/").then(state.config.set); - } - }, []); - - useEffect(() => { - if (state.auth.isLoggedIn()) { - setLoaded(false); - const client = state.config.createClient(); - setClient(client); - - client - .useExistingSession(state.auth.getSession()!) - .catch((err) => { - const error = takeError(err); - if (error === "Forbidden" || error === "Unauthorized") { - client.logout(true); - openScreen({ id: "signed_out" }); - } else { - setStatus(ClientStatus.DISCONNECTED); - openScreen({ id: "error", error }); - } - }) - .finally(() => setLoaded(true)); - } else { - setStatus(ClientStatus.READY); - setLoaded(true); - } - }, [state.auth.getSession()]); - - useEffect(() => registerEvents(state, setStatus, client), [client]); - - if (!loaded || status === ClientStatus.LOADING) { - return ; - } - - return ( - - - - {children} - - - - ); -}); - -export const useClient = () => useContext(AppContext); diff --git a/src/context/revoltjs/StateMonitor.tsx b/src/context/revoltjs/StateMonitor.tsx deleted file mode 100644 index 34400f0b..00000000 --- a/src/context/revoltjs/StateMonitor.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * This file monitors the message cache to delete any queued messages that have already sent. - */ -import { Message } from "revolt.js"; - -import { useContext, useEffect } from "preact/hooks"; - -import { useApplicationState } from "../../mobx/State"; - -import { setGlobalEmojiPack } from "../../components/common/Emoji"; - -import { AppContext } from "./RevoltClient"; - -export default function StateMonitor() { - const client = useContext(AppContext); - const state = useApplicationState(); - - useEffect(() => { - function add(msg: Message) { - if (!msg.nonce) return; - if ( - !state.queue.get(msg.channel_id).find((x) => x.id === msg.nonce) - ) - return; - state.queue.remove(msg.nonce); - } - - client.addListener("message", add); - return () => client.removeListener("message", add); - }, [client]); - - // Set global emoji pack. - useEffect(() => { - const v = state.settings.get("appearance:emoji"); - v && setGlobalEmojiPack(v); - }, [state.settings.get("appearance:emoji")]); - - return null; -} diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx deleted file mode 100644 index 3137cad9..00000000 --- a/src/context/revoltjs/SyncManager.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/** - * This file monitors changes to settings and syncs them to the server. - */ -import { ClientboundNotification } from "revolt.js"; - -import { useEffect } from "preact/hooks"; - -import { reportError } from "../../lib/ErrorBoundary"; - -import { useApplicationState } from "../../mobx/State"; - -import { useClient } from "./RevoltClient"; - -export default function SyncManager() { - const client = useClient(); - const state = useApplicationState(); - - // Sync settings from Revolt. - useEffect(() => { - if (client) { - state.sync.pull(client); - } - }, [client]); - - // Keep data synced. - useEffect(() => state.registerListeners(client), [client]); - - // Take data updates from Revolt. - useEffect(() => { - if (!client) return; - function onPacket(packet: ClientboundNotification) { - if (packet.type === "UserSettingsUpdate") { - try { - state.sync.apply(packet.update); - } catch (err) { - reportError(err as any, "failed_sync_apply"); - } - } - } - - client.addListener("packet", onPacket); - return () => client.removeListener("packet", onPacket); - }, [client]); - - return <>; -} diff --git a/src/context/revoltjs/events.ts b/src/context/revoltjs/events.ts deleted file mode 100644 index a4121bc9..00000000 --- a/src/context/revoltjs/events.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Client, Server } from "revolt.js"; - -import { StateUpdater } from "preact/hooks"; - -import { deleteRenderer } from "../../lib/renderer/Singleton"; - -import State from "../../mobx/State"; - -import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar"; -import { ClientStatus } from "./RevoltClient"; - -export function registerEvents( - state: State, - setStatus: StateUpdater, - client: Client, -) { - if (!client) return; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let listeners: Record void> = { - connecting: () => setStatus(ClientStatus.CONNECTING), - dropped: () => setStatus(ClientStatus.DISCONNECTED), - - ready: () => { - resetMemberSidebarFetched(); - setStatus(ClientStatus.ONLINE); - }, - - logout: () => { - state.auth.logout(); - state.reset(); - setStatus(ClientStatus.READY); - }, - - "channel/delete": (channel_id: string) => { - deleteRenderer(channel_id); - }, - - "server/delete": (_, server: Server) => { - if (server) { - for (const channel_id of server.channel_ids) { - deleteRenderer(channel_id); - } - } - }, - }; - - if (import.meta.env.DEV) { - listeners = new Proxy(listeners, { - get: - (target, listener) => - (...args: unknown[]) => { - console.debug(`Calling ${listener.toString()} with`, args); - Reflect.get(target, listener)(...args); - }, - }); - } - - // TODO: clean this a bit and properly handle types - for (const listener in listeners) { - client.addListener(listener, listeners[listener]); - } - - const online = () => { - setStatus(ClientStatus.RECONNECTING); - client.options.autoReconnect = false; - client.websocket.connect(); - }; - - const offline = () => { - client.options.autoReconnect = false; - client.websocket.disconnect(); - }; - - window.addEventListener("online", online); - window.addEventListener("offline", offline); - - return () => { - for (const listener in listeners) { - client.removeListener( - listener, - listeners[listener as keyof typeof listeners], - ); - } - - window.removeEventListener("online", online); - window.removeEventListener("offline", offline); - }; -} diff --git a/src/context/revoltjs/util.tsx b/src/context/revoltjs/util.tsx deleted file mode 100644 index 106fefe9..00000000 --- a/src/context/revoltjs/util.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Channel } from "revolt.js"; - -import { Text } from "preact-i18n"; - -import { Children } from "../../types/Preact"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function takeError(error: any): string { - if (error.response) { - const status = error.response.status; - if (error.response.type) { - return error.response.type; - } - - switch (status) { - case 429: - return "TooManyRequests"; - case 401: - case 403: - return "Unauthorized"; - default: - return "UnknownError"; - } - } else if (error.request) { - return "NetworkError"; - } - - console.error(error); - return "UnknownError"; -} - -export function getChannelName( - channel: Channel, - prefixType?: boolean, -): Children { - if (channel.channel_type === "SavedMessages") - return ; - - if (channel.channel_type === "DirectMessage") { - return ( - <> - {prefixType && "@"} - {channel.recipient!.username} - - ); - } - - if (channel.channel_type === "TextChannel" && prefixType) { - return <>#{channel.name}; - } - - return <>{channel.name}; -} diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx new file mode 100644 index 00000000..fc51c4f6 --- /dev/null +++ b/src/controllers/client/ClientController.tsx @@ -0,0 +1,316 @@ +import { detect } from "detect-browser"; +import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; +import { API, Client, Nullable } from "revolt.js"; + +import { injectController } from "../../lib/window"; + +import { state } from "../../mobx/State"; +import Auth from "../../mobx/stores/Auth"; + +import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar"; +import { modalController } from "../modals/ModalController"; +import Session from "./Session"; + +/** + * Controls the lifecycles of clients + */ +class ClientController { + /** + * API client + */ + private apiClient: Client; + + /** + * Server configuration + */ + private configuration: API.RevoltConfig | null; + + /** + * Map of user IDs to sessions + */ + private sessions: ObservableMap; + + /** + * User ID of active session + */ + private current: Nullable; + + constructor() { + this.apiClient = new Client({ + apiURL: import.meta.env.VITE_API_URL, + }); + + // ! FIXME: loop until success infinitely + this.apiClient + .fetchConfiguration() + .then(() => (this.configuration = this.apiClient.configuration!)); + + this.configuration = null; + this.sessions = new ObservableMap(); + this.current = null; + + makeAutoObservable(this); + + this.login = this.login.bind(this); + this.logoutCurrent = this.logoutCurrent.bind(this); + + // Inject globally + injectController("client", this); + } + + @action pickNextSession() { + this.switchAccount( + this.current ?? this.sessions.keys().next().value ?? null, + ); + } + + /** + * Hydrate sessions and start client lifecycles. + * @param auth Authentication store + */ + @action hydrate(auth: Auth) { + for (const entry of auth.getAccounts()) { + this.addSession(entry, "existing"); + } + + this.pickNextSession(); + } + + /** + * Get the currently selected session + * @returns Active Session + */ + @computed getActiveSession() { + return this.sessions.get(this.current!); + } + + /** + * Get the currently ready client + * @returns Ready Client + */ + @computed getReadyClient() { + const session = this.getActiveSession(); + return session && session.ready ? session.client! : undefined; + } + + /** + * Get an unauthenticated instance of the Revolt.js Client + * @returns API Client + */ + @computed getAnonymousClient() { + return this.apiClient; + } + + /** + * Get the next available client (either from session or API) + * @returns Revolt.js Client + */ + @computed getAvailableClient() { + return this.getActiveSession()?.client ?? this.apiClient; + } + + /** + * Fetch server configuration + * @returns Server Configuration + */ + @computed getServerConfig() { + return this.configuration; + } + + /** + * Check whether we are logged in right now + * @returns Whether we are logged in + */ + @computed isLoggedIn() { + return this.current !== null; + } + + /** + * Check whether we are currently ready + * @returns Whether we are ready to render + */ + @computed isReady() { + return this.getActiveSession()?.ready; + } + + /** + * Start a new client lifecycle + * @param entry Session Information + * @param knowledge Whether the session is new or existing + */ + @action addSession( + entry: { session: SessionPrivate; apiUrl?: string }, + knowledge: "new" | "existing", + ) { + const user_id = entry.session.user_id!; + + const session = new Session(); + this.sessions.set(user_id, session); + this.pickNextSession(); + + session + .emit({ + action: "LOGIN", + session: entry.session, + apiUrl: entry.apiUrl, + configuration: this.configuration!, + knowledge, + }) + .catch((error) => { + if (error === "Forbidden" || error === "Unauthorized") { + this.sessions.delete(user_id); + state.auth.removeSession(user_id); + modalController.push({ type: "signed_out" }); + session.destroy(); + } + }); + } + + /** + * Login given a set of credentials + * @param credentials Credentials + */ + async login(credentials: API.DataLogin) { + const browser = detect(); + + // Generate a friendly name for this browser + let friendly_name; + if (browser) { + let { name } = browser; + const { os } = browser; + let isiPad; + if (window.isNative) { + friendly_name = `Revolt Desktop on ${os}`; + } else { + if (name === "ios") { + name = "safari"; + } else if (name === "fxios") { + name = "firefox"; + } else if (name === "crios") { + name = "chrome"; + } + if (os === "Mac OS" && navigator.maxTouchPoints > 0) + isiPad = true; + friendly_name = `${name} on ${isiPad ? "iPadOS" : os}`; + } + } else { + friendly_name = "Unknown Device"; + } + + // Try to login with given credentials + let session = await this.apiClient.api.post("/auth/session/login", { + ...credentials, + friendly_name, + }); + + // Prompt for MFA verificaiton if necessary + if (session.result === "MFA") { + const { allowed_methods } = session; + while (session.result === "MFA") { + const mfa_response: API.MFAResponse | undefined = + await new Promise((callback) => + modalController.push({ + type: "mfa_flow", + state: "unknown", + available_methods: allowed_methods, + callback, + }), + ); + + if (typeof mfa_response === "undefined") { + break; + } + + try { + session = await this.apiClient.api.post( + "/auth/session/login", + { + mfa_response, + mfa_ticket: session.ticket, + friendly_name, + }, + ); + } catch (err) { + console.error("Failed login:", err); + } + } + + if (session.result === "MFA") { + throw "Cancelled"; + } + } + + // Start client lifecycle + this.addSession( + { + session, + }, + "new", + ); + } + + /** + * Log out of a specific user session + * @param user_id Target User ID + */ + @action logout(user_id: string) { + const session = this.sessions.get(user_id); + if (session) { + if (user_id === this.current) { + this.current = null; + } + + this.sessions.delete(user_id); + this.pickNextSession(); + session.destroy(); + } + } + + /** + * Logout of the current session + */ + @action logoutCurrent() { + if (this.current) { + this.logout(this.current); + } + } + + /** + * Switch to another user session + * @param user_id Target User ID + */ + @action switchAccount(user_id: string) { + this.current = user_id; + + // This will allow account switching to work more seamlessly, + // maybe it'll be properly / fully implemented at some point. + resetMemberSidebarFetched(); + } +} + +export const clientController = new ClientController(); + +/** + * Get the currently active session. + * @returns Session + */ +export function useSession() { + return clientController.getActiveSession(); +} + +/** + * Get the currently active client or an unauthorised + * client for API requests, whichever is available. + * @returns Revolt.js Client + */ +export function useClient() { + return clientController.getAvailableClient(); +} + +/** + * Get unauthorised client for API requests. + * @returns Revolt.js Client + */ +export function useApi() { + return clientController.getAnonymousClient().api; +} diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx new file mode 100644 index 00000000..f1360e26 --- /dev/null +++ b/src/controllers/client/Session.tsx @@ -0,0 +1,275 @@ +import { action, computed, makeAutoObservable } from "mobx"; +import { API, Client } from "revolt.js"; + +import { state } from "../../mobx/State"; + +import { modalController } from "../modals/ModalController"; + +/** + * Current lifecycle state + */ +type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; + +/** + * Possible transitions between states + */ +type Transition = + | { + action: "LOGIN"; + apiUrl?: string; + session: SessionPrivate; + configuration?: API.RevoltConfig; + + knowledge: "new" | "existing"; + } + | { + action: + | "SUCCESS" + | "DISCONNECT" + | "RETRY" + | "LOGOUT" + | "ONLINE" + | "OFFLINE"; + }; + +/** + * Client lifecycle finite state machine + */ +export default class Session { + state: State = window.navigator.onLine ? "Ready" : "Offline"; + user_id: string | null = null; + client: Client | null = null; + + /** + * Create a new Session + */ + constructor() { + makeAutoObservable(this); + + this.onDropped = this.onDropped.bind(this); + this.onReady = this.onReady.bind(this); + this.onOnline = this.onOnline.bind(this); + this.onOffline = this.onOffline.bind(this); + + window.addEventListener("online", this.onOnline); + window.addEventListener("offline", this.onOffline); + } + + /** + * Initiate logout and destroy client + */ + @action destroy() { + if (this.client) { + this.client.logout(false); + this.state = "Ready"; + this.client = null; + } + } + + /** + * Called when user's browser signals it is online + */ + private onOnline() { + this.emit({ + action: "ONLINE", + }); + } + + /** + * Called when user's browser signals it is offline + */ + private onOffline() { + this.emit({ + action: "OFFLINE", + }); + } + + /** + * Called when the client signals it has disconnected + */ + private onDropped() { + this.emit({ + action: "DISCONNECT", + }); + } + + /** + * Called when the client signals it has received the Ready packet + */ + private onReady() { + this.emit({ + action: "SUCCESS", + }); + } + + /** + * Create a new Revolt.js Client for this Session + * @param apiUrl Optionally specify an API URL + */ + private createClient(apiUrl?: string) { + this.client = new Client({ + unreads: true, + autoReconnect: false, + onPongTimeout: "EXIT", + apiURL: apiUrl ?? import.meta.env.VITE_API_URL, + }); + + this.client.addListener("dropped", this.onDropped); + this.client.addListener("ready", this.onReady); + } + + /** + * Destroy the client including any listeners. + */ + private destroyClient() { + this.client!.removeAllListeners(); + this.client!.logout(); + this.user_id = null; + this.client = null; + } + + /** + * Ensure we are in one of the given states + * @param state Possible states + */ + private assert(...state: State[]) { + let found = false; + for (const target of state) { + if (this.state === target) { + found = true; + break; + } + } + + if (!found) { + throw `State must be ${state} in order to transition! (currently ${this.state})`; + } + } + + /** + * Continue logging in provided onboarding is successful + * @param data Transition Data + */ + private async continueLogin(data: Transition & { action: "LOGIN" }) { + try { + await this.client!.useExistingSession(data.session); + this.user_id = this.client!.user!._id; + state.auth.setSession(data.session); + } catch (err) { + this.state = "Ready"; + throw err; + } + } + + /** + * Transition to a new state by a certain action + * @param data Transition Data + */ + @action async emit(data: Transition) { + console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data); + + switch (data.action) { + // Login with session + case "LOGIN": { + this.assert("Ready"); + this.state = "Connecting"; + this.createClient(data.apiUrl); + + if (data.configuration) { + this.client!.configuration = data.configuration; + } + + if (data.knowledge === "new") { + await this.client!.fetchConfiguration(); + this.client!.session = data.session; + (this.client! as any).$updateHeaders(); + + const { onboarding } = await this.client!.api.get( + "/onboard/hello", + ); + + if (onboarding) { + modalController.push({ + type: "onboarding", + callback: async (username: string) => + this.client!.completeOnboarding( + { username }, + false, + ).then(() => this.continueLogin(data)), + }); + + return; + } + } + + this.continueLogin(data); + + break; + } + // Ready successfully received + case "SUCCESS": { + this.assert("Connecting"); + this.state = "Online"; + break; + } + // Client got disconnected + case "DISCONNECT": { + if (navigator.onLine) { + this.assert("Online"); + this.state = "Disconnected"; + + setTimeout(() => { + // Check we are still disconnected before retrying. + if (this.state === "Disconnected") { + this.emit({ + action: "RETRY", + }); + } + }, 1000); + } + + break; + } + // We should try reconnecting + case "RETRY": { + this.assert("Disconnected"); + this.client!.websocket.connect(); + this.state = "Connecting"; + break; + } + // User instructed logout + case "LOGOUT": { + this.assert("Connecting", "Online", "Disconnected"); + this.state = "Ready"; + this.destroyClient(); + break; + } + // Browser went offline + case "OFFLINE": { + this.state = "Offline"; + break; + } + // Browser went online + case "ONLINE": { + this.assert("Offline"); + if (this.client) { + this.state = "Disconnected"; + this.emit({ + action: "RETRY", + }); + } else { + this.state = "Ready"; + } + break; + } + } + } + + /** + * Whether we are ready to render. + * @returns Boolean + */ + @computed get ready() { + return !!this.client?.user; + } +} diff --git a/src/controllers/client/jsx/Binder.tsx b/src/controllers/client/jsx/Binder.tsx new file mode 100644 index 00000000..cbec7403 --- /dev/null +++ b/src/controllers/client/jsx/Binder.tsx @@ -0,0 +1,18 @@ +import { observer } from "mobx-react-lite"; + +import { useEffect } from "preact/hooks"; + +import { state } from "../../../mobx/State"; + +import { clientController } from "../ClientController"; + +/** + * Also binds listeners from state to the current client. + */ +const Binder: React.FC = () => { + const client = clientController.getReadyClient(); + useEffect(() => state.registerListeners(client!), [client]); + return null; +}; + +export default observer(Binder); diff --git a/src/controllers/client/jsx/ChannelName.tsx b/src/controllers/client/jsx/ChannelName.tsx new file mode 100644 index 00000000..d695eeac --- /dev/null +++ b/src/controllers/client/jsx/ChannelName.tsx @@ -0,0 +1,34 @@ +// ! This should be moved into @revoltchat/ui +import { Channel } from "revolt.js"; + +import { Text } from "preact-i18n"; + +interface Props { + channel?: Channel; + prefix?: boolean; +} + +/** + * Channel display name + */ +export function ChannelName({ channel, prefix }: Props) { + if (!channel) return <>; + + if (channel.channel_type === "SavedMessages") + return ; + + if (channel.channel_type === "DirectMessage") { + return ( + <> + {prefix && "@"} + {channel.recipient!.username} + + ); + } + + if (channel.channel_type === "TextChannel" && prefix) { + return <>{`#${channel.name}`}; + } + + return <>{channel.name}; +} diff --git a/src/controllers/client/jsx/CheckAuth.tsx b/src/controllers/client/jsx/CheckAuth.tsx new file mode 100644 index 00000000..546801a3 --- /dev/null +++ b/src/controllers/client/jsx/CheckAuth.tsx @@ -0,0 +1,41 @@ +import { observer } from "mobx-react-lite"; +import { Redirect } from "react-router-dom"; + +import { Preloader } from "@revoltchat/ui"; + +import { clientController } from "../ClientController"; + +interface Props { + auth?: boolean; + blockRender?: boolean; + + children: Children; +} + +/** + * Check that we are logged in or out and redirect accordingly. + * Also prevent render until the client is ready to display. + */ +export const CheckAuth = observer((props: Props) => { + const loggedIn = clientController.isLoggedIn(); + + // Redirect if logged out on authenticated page or vice-versa. + if (props.auth && !loggedIn) { + if (props.blockRender) return null; + return ; + } else if (!props.auth && loggedIn) { + if (props.blockRender) return null; + return ; + } + + // Block render if client is getting ready to work. + if ( + props.auth && + clientController.isLoggedIn() && + !clientController.isReady() + ) { + return ; + } + + return <>{props.children}; +}); diff --git a/src/context/revoltjs/RequiresOnline.tsx b/src/controllers/client/jsx/RequiresOnline.tsx similarity index 66% rename from src/context/revoltjs/RequiresOnline.tsx rename to src/controllers/client/jsx/RequiresOnline.tsx index f3595a5d..d8ed18ec 100644 --- a/src/context/revoltjs/RequiresOnline.tsx +++ b/src/controllers/client/jsx/RequiresOnline.tsx @@ -2,12 +2,10 @@ import { WifiOff } from "@styled-icons/boxicons-regular"; import styled from "styled-components/macro"; import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; -import Preloader from "../../components/ui/Preloader"; +import { Preloader } from "@revoltchat/ui"; -import { Children } from "../../types/Preact"; -import { ClientStatus, StatusContext } from "./RevoltClient"; +import { useSession } from "../ClientController"; interface Props { children: Children; @@ -30,10 +28,12 @@ const Base = styled.div` `; export default function RequiresOnline(props: Props) { - const status = useContext(StatusContext); + const session = useSession(); - if (status === ClientStatus.CONNECTING) return ; - if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY) + if (!session || session.state === "Connecting") + return ; + + if (!(session.state === "Online" || session.state === "Ready")) return ( diff --git a/src/controllers/client/jsx/error.tsx b/src/controllers/client/jsx/error.tsx new file mode 100644 index 00000000..c171c55a --- /dev/null +++ b/src/controllers/client/jsx/error.tsx @@ -0,0 +1,29 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function takeError(error: any): string { + if (error.response) { + const type = error.response.data?.type; + if (type) { + return type; + } + + switch (error.response.status) { + case 429: + return "TooManyRequests"; + case 401: + case 403: + return "Unauthorized"; + default: + return "UnknownError"; + } + } else if (error.request) { + return "NetworkError"; + } + + console.error(error); + return "UnknownError"; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mapError(error: any): never { + throw takeError(error); +} diff --git a/src/context/revoltjs/FileUploads.module.scss b/src/controllers/client/jsx/legacy/FileUploads.module.scss similarity index 100% rename from src/context/revoltjs/FileUploads.module.scss rename to src/controllers/client/jsx/legacy/FileUploads.module.scss diff --git a/src/context/revoltjs/FileUploads.tsx b/src/controllers/client/jsx/legacy/FileUploads.tsx similarity index 79% rename from src/context/revoltjs/FileUploads.tsx rename to src/controllers/client/jsx/legacy/FileUploads.tsx index 5fef3e12..ec36bb6f 100644 --- a/src/context/revoltjs/FileUploads.tsx +++ b/src/controllers/client/jsx/legacy/FileUploads.tsx @@ -5,20 +5,23 @@ import Axios, { AxiosRequestConfig } from "axios"; import styles from "./FileUploads.module.scss"; import classNames from "classnames"; import { Text } from "preact-i18n"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; -import { determineFileSize } from "../../lib/fileSize"; +import { IconButton, Preloader } from "@revoltchat/ui"; -import IconButton from "../../components/ui/IconButton"; -import Preloader from "../../components/ui/Preloader"; +import { determineFileSize } from "../../../../lib/fileSize"; -import { useIntermediate } from "../intermediate/Intermediate"; -import { AppContext } from "./RevoltClient"; -import { takeError } from "./util"; +import { modalController } from "../../../modals/ModalController"; +import { useClient } from "../../ClientController"; +import { takeError } from "../error"; type BehaviourType = | { behaviour: "ask"; onChange: (file: File) => void } - | { behaviour: "upload"; onUpload: (id: string) => Promise } + | { + behaviour: "upload"; + onUpload: (id: string) => Promise; + previewAfterUpload?: boolean; + } | { behaviour: "multi"; onChange: (files: File[]) => void; @@ -49,7 +52,8 @@ type Props = BehaviourType & | "icons" | "avatars" | "attachments" - | "banners"; + | "banners" + | "emojis"; maxFileSize: number; remove: () => Promise; }; @@ -73,7 +77,7 @@ export async function uploadFile( return res.data.id; } -var input: HTMLInputElement; +let input: HTMLInputElement; export function grabFiles( maxFileSize: number, cb: (files: File[]) => void, @@ -112,10 +116,22 @@ export function grabFiles( export function FileUploader(props: Props) { const { fileType, maxFileSize, remove } = props; - const { openScreen } = useIntermediate(); - const client = useContext(AppContext); + const client = useClient(); const [uploading, setUploading] = useState(false); + const [previewFile, setPreviewFile] = useState(null!); + const [generatedPreviewURL, setGeneratedPreviewURL] = useState< + string | undefined + >(undefined); + useEffect(() => { + if (previewFile) { + const url: string = URL.createObjectURL(previewFile); + setGeneratedPreviewURL(url); + return () => URL.revokeObjectURL(url); + } + + setGeneratedPreviewURL(""); + }, [previewFile]); function onClick() { if (uploading) return; @@ -138,14 +154,25 @@ export function FileUploader(props: Props) { files[0], ), ); + + if (props.previewAfterUpload) { + setPreviewFile(files[0]); + } } } catch (err) { - return openScreen({ id: "error", error: takeError(err) }); + return modalController.push({ + type: "error", + error: takeError(err), + }); } finally { setUploading(false); } }, - () => openScreen({ id: "error", error: "FileTooLarge" }), + () => + modalController.push({ + type: "error", + error: "FileTooLarge", + }), props.behaviour === "multi", ); } @@ -159,7 +186,11 @@ export function FileUploader(props: Props) { } else { onClick(); } - } else if (props.previewURL) { + } else if (props.previewURL || previewFile) { + if (previewFile) { + setPreviewFile(null!); + } + props.remove(); } else { onClick(); @@ -181,8 +212,8 @@ export function FileUploader(props: Props) { const blob = item.getAsFile(); if (blob) { if (blob.size > props.maxFileSize) { - openScreen({ - id: "error", + modalController.push({ + type: "error", error: "FileTooLarge", }); continue; @@ -213,7 +244,10 @@ export function FileUploader(props: Props) { const files = []; for (const item of dropped) { if (item.size > props.maxFileSize) { - openScreen({ id: "error", error: "FileTooLarge" }); + modalController.push({ + type: "error", + error: "FileTooLarge", + }); continue; } @@ -233,7 +267,7 @@ export function FileUploader(props: Props) { document.removeEventListener("dragover", dragover); document.removeEventListener("drop", drop); }; - }, [openScreen, props, props.append]); + }, [props, props.append]); } if (props.style === "icon" || props.style === "banner") { @@ -244,6 +278,9 @@ export function FileUploader(props: Props) { [styles.icon]: style === "icon", [styles.banner]: style === "banner", })} + style={{ + alignItems: props.style === "icon" ? "center" : "none", + }} data-uploading={uploading}>
{uploading ? ( - ) : props.previewURL ? ( + ) : props.previewURL || previewFile ? ( ) : ( diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx new file mode 100644 index 00000000..c70f0487 --- /dev/null +++ b/src/controllers/modals/ModalController.tsx @@ -0,0 +1,278 @@ +import { + action, + computed, + makeObservable, + observable, + runInAction, +} from "mobx"; +import type { Client, API } from "revolt.js"; +import { ulid } from "ulid"; + +import { determineLink } from "../../lib/links"; +import { injectController } from "../../lib/window"; + +import { getApplicationState } from "../../mobx/State"; + +import { history } from "../../context/history"; + +import AddFriend from "./components/AddFriend"; +import BanMember from "./components/BanMember"; +import Changelog from "./components/Changelog"; +import ChannelInfo from "./components/ChannelInfo"; +import Clipboard from "./components/Clipboard"; +import Confirmation from "./components/Confirmation"; +import CreateBot from "./components/CreateBot"; +import CreateCategory from "./components/CreateCategory"; +import CreateChannel from "./components/CreateChannel"; +import CreateGroup from "./components/CreateGroup"; +import CreateInvite from "./components/CreateInvite"; +import CreateRole from "./components/CreateRole"; +import CreateServer from "./components/CreateServer"; +import CustomStatus from "./components/CustomStatus"; +import DeleteMessage from "./components/DeleteMessage"; +import Error from "./components/Error"; +import ImageViewer from "./components/ImageViewer"; +import KickMember from "./components/KickMember"; +import LinkWarning from "./components/LinkWarning"; +import MFAEnableTOTP from "./components/MFAEnableTOTP"; +import MFAFlow from "./components/MFAFlow"; +import MFARecovery from "./components/MFARecovery"; +import ModifyAccount from "./components/ModifyAccount"; +import OutOfDate from "./components/OutOfDate"; +import PendingFriendRequests from "./components/PendingFriendRequests"; +import ServerIdentity from "./components/ServerIdentity"; +import ServerInfo from "./components/ServerInfo"; +import ShowToken from "./components/ShowToken"; +import SignOutSessions from "./components/SignOutSessions"; +import SignedOut from "./components/SignedOut"; +import UserPicker from "./components/UserPicker"; +import { OnboardingModal } from "./components/legacy/Onboarding"; +import { UserProfile } from "./components/legacy/UserProfile"; +import { Modal } from "./types"; + +type Components = Record>; + +/** + * Handles layering and displaying modals to the user. + */ +class ModalController { + stack: T[] = []; + components: Components; + + constructor(components: Components) { + this.components = components; + + makeObservable(this, { + stack: observable, + push: action, + pop: action, + remove: action, + rendered: computed, + isVisible: computed, + }); + + this.close = this.close.bind(this); + + // Inject globally + injectController("modal", this); + } + + /** + * Display a new modal on the stack + * @param modal Modal data + */ + push(modal: T) { + this.stack = [ + ...this.stack, + { + ...modal, + key: ulid(), + }, + ]; + } + + /** + * Remove the top modal from the screen + * @param signal What action to trigger + */ + pop(signal: "close" | "confirm" | "force") { + this.stack = this.stack.map((entry, index) => + index === this.stack.length - 1 ? { ...entry, signal } : entry, + ); + } + + /** + * Close the top modal + */ + close() { + this.pop("close"); + } + + /** + * Remove the keyed modal from the stack + */ + remove(key: string) { + this.stack = this.stack.filter((x) => x.key !== key); + } + + /** + * Render modals + */ + get rendered() { + return ( + <> + {this.stack.map((modal) => { + const Component = this.components[modal.type]; + return ( + // ESLint does not understand spread operator + // eslint-disable-next-line + this.remove(modal.key!)} + /> + ); + })} + + ); + } + + /** + * Whether a modal is currently visible + */ + get isVisible() { + return this.stack.length > 0; + } +} + +/** + * Modal controller with additional helpers. + */ +class ModalControllerExtended extends ModalController { + /** + * Perform MFA flow + * @param client Client + */ + mfaFlow(client: Client) { + return runInAction( + () => + new Promise((callback: (ticket?: API.MFATicket) => void) => + this.push({ + type: "mfa_flow", + state: "known", + client, + callback, + }), + ), + ); + } + + /** + * Open TOTP secret modal + * @param client Client + */ + mfaEnableTOTP(secret: string, identifier: string) { + return runInAction( + () => + new Promise((callback: (value?: string) => void) => + this.push({ + type: "mfa_enable_totp", + identifier, + secret, + callback, + }), + ), + ); + } + + /** + * Write text to the clipboard + * @param text Text to write + */ + writeText(text: string) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + } else { + this.push({ + type: "clipboard", + text, + }); + } + } + + /** + * Safely open external or internal link + * @param href Raw URL + * @param trusted Whether we trust this link + * @returns Whether to cancel default event + */ + openLink(href?: string, trusted?: boolean) { + const link = determineLink(href); + const settings = getApplicationState().settings; + + switch (link.type) { + case "navigate": { + history.push(link.path); + break; + } + case "external": { + if ( + !trusted && + !settings.security.isTrustedOrigin(link.url.hostname) + ) { + modalController.push({ + type: "link_warning", + link: link.href, + callback: () => this.openLink(href, true) as true, + }); + } else { + window.open(link.href, "_blank", "noreferrer"); + } + } + } + + return true; + } +} + +export const modalController = new ModalControllerExtended({ + add_friend: AddFriend, + ban_member: BanMember, + changelog: Changelog, + channel_info: ChannelInfo, + clipboard: Clipboard, + leave_group: Confirmation, + close_dm: Confirmation, + leave_server: Confirmation, + delete_server: Confirmation, + delete_channel: Confirmation, + delete_bot: Confirmation, + block_user: Confirmation, + unfriend_user: Confirmation, + create_category: CreateCategory, + create_channel: CreateChannel, + create_group: CreateGroup, + create_invite: CreateInvite, + create_role: CreateRole, + create_server: CreateServer, + create_bot: CreateBot, + custom_status: CustomStatus, + delete_message: DeleteMessage, + error: Error, + image_viewer: ImageViewer, + kick_member: KickMember, + link_warning: LinkWarning, + mfa_flow: MFAFlow, + mfa_recovery: MFARecovery, + mfa_enable_totp: MFAEnableTOTP, + modify_account: ModifyAccount, + onboarding: OnboardingModal, + out_of_date: OutOfDate, + pending_friend_requests: PendingFriendRequests, + server_identity: ServerIdentity, + server_info: ServerInfo, + show_token: ShowToken, + signed_out: SignedOut, + sign_out_sessions: SignOutSessions, + user_picker: UserPicker, + user_profile: UserProfile, +}); diff --git a/src/controllers/modals/ModalRenderer.tsx b/src/controllers/modals/ModalRenderer.tsx new file mode 100644 index 00000000..1b51dcab --- /dev/null +++ b/src/controllers/modals/ModalRenderer.tsx @@ -0,0 +1,42 @@ +import { observer } from "mobx-react-lite"; +import { Prompt, useHistory } from "react-router-dom"; + +import { useEffect } from "preact/hooks"; + +import { modalController } from "./ModalController"; + +export default observer(() => { + const history = useHistory(); + + useEffect(() => { + function keyUp(event: KeyboardEvent) { + if (event.key === "Escape") { + modalController.pop("close"); + } else if (event.key === "Enter") { + modalController.pop("confirm"); + } + } + + document.addEventListener("keyup", keyUp); + return () => document.removeEventListener("keyup", keyUp); + }, []); + + return ( + <> + {modalController.rendered} + { + if (action === "POP") { + modalController.pop("close"); + setTimeout(() => history.push(history.location), 0); + + return false; + } + + return true; + }} + /> + + ); +}); diff --git a/src/controllers/modals/components/AddFriend.tsx b/src/controllers/modals/components/AddFriend.tsx new file mode 100644 index 00000000..c2b61f90 --- /dev/null +++ b/src/controllers/modals/components/AddFriend.tsx @@ -0,0 +1,36 @@ +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { noop } from "../../../lib/js"; + +import { useClient } from "../../client/ClientController"; +import { ModalProps } from "../types"; + +/** + * Add friend modal + */ +export default function AddFriend({ ...props }: ModalProps<"add_friend">) { + const client = useClient(); + + return ( + + client.api.post(`/users/friend`, { username }).then(noop) + } + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/BanMember.tsx b/src/controllers/modals/components/BanMember.tsx new file mode 100644 index 00000000..2c5b3278 --- /dev/null +++ b/src/controllers/modals/components/BanMember.tsx @@ -0,0 +1,50 @@ +import { Text } from "preact-i18n"; + +import { Column, ModalForm } from "@revoltchat/ui"; + +import UserIcon from "../../../components/common/user/UserIcon"; +import { ModalProps } from "../types"; + +/** + * Ban member modal + */ +export default function BanMember({ + member, + ...props +}: ModalProps<"ban_member">) { + return ( + } + schema={{ + member: "custom", + reason: "text", + }} + data={{ + member: { + element: ( + + + + + ), + }, + reason: { + field: ( + + ) as React.ReactChild, + }, + }} + callback={async ({ reason }) => + void (await member.server!.banUser(member._id.user, { reason })) + } + submit={{ + palette: "error", + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/Changelog.tsx b/src/controllers/modals/components/Changelog.tsx new file mode 100644 index 00000000..217ba109 --- /dev/null +++ b/src/controllers/modals/components/Changelog.tsx @@ -0,0 +1,107 @@ +import dayjs from "dayjs"; +import styled from "styled-components"; + +import { Text } from "preact-i18n"; +import { useMemo, useState } from "preact/hooks"; + +import { CategoryButton, Column, Modal } from "@revoltchat/ui"; +import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal"; + +import { noopTrue } from "../../../lib/js"; + +import { + changelogEntries, + changelogEntryArray, + ChangelogPost, +} from "../../../assets/changelogs"; +import { ModalProps } from "../types"; + +const Image = styled.img` + border-radius: var(--border-radius); +`; + +function RenderLog({ post }: { post: ChangelogPost }) { + return ( + + {post.content.map((entry) => + typeof entry === "string" ? ( + {entry} + ) : ( + + ), + )} + + ); +} + +/** + * Changelog modal + */ +export default function Changelog({ + initial, + onClose, + signal, +}: ModalProps<"changelog">) { + const [log, setLog] = useState(initial); + + const entry = useMemo( + () => (log ? changelogEntries[log] : undefined), + [log], + ); + + const actions = useMemo(() => { + const arr: Action[] = [ + { + palette: "primary", + children: , + onClick: noopTrue, + }, + ]; + + if (log) { + arr.push({ + palette: "plain-secondary", + children: , + onClick: () => { + setLog(undefined); + return false; + }, + }); + } + + return arr; + }, [log]); + + return ( + + ) + } + description={ + entry ? ( + dayjs(entry.date).calendar() + ) : ( + + ) + } + actions={actions} + onClose={onClose} + signal={signal}> + {entry ? ( + + ) : ( + + {changelogEntryArray.map((entry, index) => ( + setLog(index + 1)}> + {entry.title} + + ))} + + )} + + ); +} diff --git a/src/controllers/modals/components/ChannelInfo.tsx b/src/controllers/modals/components/ChannelInfo.tsx new file mode 100644 index 00000000..0e2149b0 --- /dev/null +++ b/src/controllers/modals/components/ChannelInfo.tsx @@ -0,0 +1,29 @@ +import { X } from "@styled-icons/boxicons-regular"; + +import { Column, H1, IconButton, Modal, Row } from "@revoltchat/ui"; + +import Markdown from "../../../components/markdown/Markdown"; +import { modalController } from "../ModalController"; +import { ModalProps } from "../types"; + +export default function ChannelInfo({ + channel, + ...props +}: ModalProps<"channel_info">) { + return ( + + +

{`#${channel.name}`}

+
+ + + + + }> + +
+ ); +} diff --git a/src/controllers/modals/components/Clipboard.tsx b/src/controllers/modals/components/Clipboard.tsx new file mode 100644 index 00000000..8758319f --- /dev/null +++ b/src/controllers/modals/components/Clipboard.tsx @@ -0,0 +1,32 @@ +import { Text } from "preact-i18n"; + +import { Modal } from "@revoltchat/ui"; + +import { noopTrue } from "../../../lib/js"; + +import { ModalProps } from "../types"; + +export default function Clipboard({ text, ...props }: ModalProps<"clipboard">) { + return ( + } + description={ + location.protocol !== "https:" ? ( + + ) : undefined + } + actions={[ + { + onClick: noopTrue, + confirmation: true, + children: , + }, + ]}> + {" "} + + {text} + + + ); +} diff --git a/src/controllers/modals/components/Confirmation.tsx b/src/controllers/modals/components/Confirmation.tsx new file mode 100644 index 00000000..5e7977c9 --- /dev/null +++ b/src/controllers/modals/components/Confirmation.tsx @@ -0,0 +1,107 @@ +import { useHistory } from "react-router-dom"; + +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { TextReact } from "../../../lib/i18n"; + +import { clientController } from "../../client/ClientController"; +import { ModalProps } from "../types"; + +/** + * Confirmation modal + */ +export default function Confirmation( + props: ModalProps< + | "leave_group" + | "close_dm" + | "leave_server" + | "delete_server" + | "delete_channel" + | "delete_bot" + | "block_user" + | "unfriend_user" + >, +) { + const history = useHistory(); + + const EVENTS = { + close_dm: ["confirm_close_dm", "close"], + delete_server: ["confirm_delete", "delete"], + delete_channel: ["confirm_delete", "delete"], + delete_bot: ["confirm_delete", "delete"], + leave_group: ["confirm_leave", "leave"], + leave_server: ["confirm_leave", "leave"], + unfriend_user: ["unfriend_user", "remove"], + block_user: ["block_user", "block"], + }; + + const event = EVENTS[props.type]; + let name; + switch (props.type) { + case "unfriend_user": + case "block_user": + name = props.target.username; + break; + case "close_dm": + name = props.target.recipient?.username; + break; + case "delete_bot": + name = props.name; + break; + default: + name = props.target.name; + } + + return ( + + } + description={ + {name} }} + /> + } + data={{}} + schema={{}} + callback={async () => { + switch (props.type) { + case "unfriend_user": + await props.target.removeFriend(); + break; + case "block_user": + await props.target.blockUser(); + break; + case "leave_group": + case "close_dm": + case "delete_channel": + case "leave_server": + case "delete_server": + if (props.type != "delete_channel") history.push("/"); + + props.target.delete(); + break; + case "delete_bot": + clientController + .getAvailableClient() + .bots.delete(props.target); + props.cb?.(); + break; + } + }} + submit={{ + palette: "error", + children: ( + + ), + }} + /> + ); +} diff --git a/src/controllers/modals/components/CreateBot.tsx b/src/controllers/modals/components/CreateBot.tsx new file mode 100644 index 00000000..e5bcf791 --- /dev/null +++ b/src/controllers/modals/components/CreateBot.tsx @@ -0,0 +1,42 @@ +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { useClient } from "../../client/ClientController"; +import { mapError } from "../../client/jsx/error"; +import { ModalProps } from "../types"; + +/** + * Bot creation modal + */ +export default function CreateBot({ + onCreate, + ...props +}: ModalProps<"create_bot">) { + const client = useClient(); + + return ( + } + schema={{ + name: "text", + }} + data={{ + name: { + field: () as React.ReactChild, + }, + }} + callback={async ({ name }) => { + const { bot } = await client.bots + .create({ name }) + .catch(mapError); + + onCreate(bot); + }} + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/CreateCategory.tsx b/src/controllers/modals/components/CreateCategory.tsx new file mode 100644 index 00000000..e18aa7a9 --- /dev/null +++ b/src/controllers/modals/components/CreateCategory.tsx @@ -0,0 +1,47 @@ +import { ulid } from "ulid"; + +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { ModalProps } from "../types"; + +/** + * Category creation modal + */ +export default function CreateCategory({ + target, + ...props +}: ModalProps<"create_category">) { + return ( + } + schema={{ + name: "text", + }} + data={{ + name: { + field: ( + + ) as React.ReactChild, + }, + }} + callback={async ({ name }) => { + await target.edit({ + categories: [ + ...(target.categories ?? []), + { + id: ulid(), + title: name, + channels: [], + }, + ], + }); + }} + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/CreateChannel.tsx b/src/controllers/modals/components/CreateChannel.tsx new file mode 100644 index 00000000..e1d00e65 --- /dev/null +++ b/src/controllers/modals/components/CreateChannel.tsx @@ -0,0 +1,75 @@ +import { useHistory } from "react-router-dom"; + +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { ModalProps } from "../types"; + +/** + * Channel creation modal + */ +export default function CreateChannel({ + cb, + target, + ...props +}: ModalProps<"create_channel">) { + const history = useHistory(); + + return ( + } + schema={{ + name: "text", + type: "radio", + }} + data={{ + name: { + field: ( + + ) as React.ReactChild, + }, + type: { + field: ( + + ) as React.ReactChild, + choices: [ + { + name: ( + + ) as React.ReactChild, + value: "Text", + }, + { + name: ( + + ) as React.ReactChild, + value: "Voice", + }, + ], + }, + }} + defaults={{ + type: "Text", + }} + callback={async ({ name, type }) => { + const channel = await target.createChannel({ + type: type as "Text" | "Voice", + name, + }); + + if (cb) { + cb(channel as any); + } else { + history.push( + `/server/${target._id}/channel/${channel._id}`, + ); + } + }} + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/CreateGroup.tsx b/src/controllers/modals/components/CreateGroup.tsx new file mode 100644 index 00000000..d98f3afc --- /dev/null +++ b/src/controllers/modals/components/CreateGroup.tsx @@ -0,0 +1,47 @@ +import { useHistory } from "react-router-dom"; + +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { useClient } from "../../client/ClientController"; +import { mapError } from "../../client/jsx/error"; +import { ModalProps } from "../types"; + +/** + * Group creation modal + */ +export default function CreateGroup({ ...props }: ModalProps<"create_group">) { + const history = useHistory(); + const client = useClient(); + + return ( + } + schema={{ + name: "text", + }} + data={{ + name: { + field: ( + + ) as React.ReactChild, + }, + }} + callback={async ({ name }) => { + const group = await client.channels + .createGroup({ + name, + users: [], + }) + .catch(mapError); + + history.push(`/channel/${group._id}`); + }} + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/CreateInvite.tsx b/src/controllers/modals/components/CreateInvite.tsx new file mode 100644 index 00000000..45295545 --- /dev/null +++ b/src/controllers/modals/components/CreateInvite.tsx @@ -0,0 +1,87 @@ +import styled from "styled-components"; + +import { Text } from "preact-i18n"; +import { useEffect, useState } from "preact/hooks"; + +import { ModalForm } from "@revoltchat/ui"; + +import { noopAsync } from "../../../lib/js"; + +import { takeError } from "../../client/jsx/error"; +import { modalController } from "../ModalController"; +import { ModalProps } from "../types"; + +/** + * Code block which displays invite + */ +const Invite = styled.div` + display: flex; + flex-direction: column; + + code { + padding: 1em; + user-select: all; + font-size: 1.4em; + text-align: center; + font-family: var(--monospace-font); + } +`; + +/** + * Create invite modal + */ +export default function CreateInvite({ + target, + ...props +}: ModalProps<"create_invite">) { + const [processing, setProcessing] = useState(false); + const [code, setCode] = useState("abcdef"); + + // Generate an invite code + useEffect(() => { + setProcessing(true); + + target + .createInvite() + .then(({ _id }) => setCode(_id)) + .catch((err) => + modalController.push({ type: "error", error: takeError(err) }), + ) + .finally(() => setProcessing(false)); + }, [target]); + + return ( + } + schema={{ + message: "custom", + }} + data={{ + message: { + element: processing ? ( + + ) : ( + + + {code} + + ), + }, + }} + callback={noopAsync} + submit={{ + children: , + }} + actions={[ + { + children: , + onClick: () => + modalController.writeText( + `${window.location.protocol}//${window.location.host}/invite/${code}`, + ), + }, + ]} + /> + ); +} diff --git a/src/controllers/modals/components/CreateRole.tsx b/src/controllers/modals/components/CreateRole.tsx new file mode 100644 index 00000000..51692bef --- /dev/null +++ b/src/controllers/modals/components/CreateRole.tsx @@ -0,0 +1,38 @@ +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { ModalProps } from "../types"; + +/** + * Role creation modal + */ +export default function CreateRole({ + server, + callback, + ...props +}: ModalProps<"create_role">) { + return ( + } + schema={{ + name: "text", + }} + data={{ + name: { + field: ( + + ) as React.ReactChild, + }, + }} + callback={async ({ name }) => { + const role = await server.createRole(name); + callback(role.id); + }} + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/CreateServer.tsx b/src/controllers/modals/components/CreateServer.tsx new file mode 100644 index 00000000..9e9bbff8 --- /dev/null +++ b/src/controllers/modals/components/CreateServer.tsx @@ -0,0 +1,59 @@ +import { useHistory } from "react-router-dom"; + +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { useClient } from "../../client/ClientController"; +import { mapError } from "../../client/jsx/error"; +import { ModalProps } from "../types"; + +/** + * Server creation modal + */ +export default function CreateServer({ + ...props +}: ModalProps<"create_server">) { + const history = useHistory(); + const client = useClient(); + + return ( + } + description={ +
+ By creating this server, you agree to the{" "} + + Acceptable Use Policy. + +
+ } + schema={{ + name: "text", + }} + data={{ + name: { + field: ( + + ) as React.ReactChild, + }, + }} + callback={async ({ name }) => { + const server = await client.servers + .createServer({ + name, + }) + .catch(mapError); + + history.push(`/server/${server._id}`); + }} + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/CustomStatus.tsx b/src/controllers/modals/components/CustomStatus.tsx new file mode 100644 index 00000000..4b72ae77 --- /dev/null +++ b/src/controllers/modals/components/CustomStatus.tsx @@ -0,0 +1,46 @@ +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { useClient } from "../../client/ClientController"; +import { ModalProps } from "../types"; + +/** + * Custom status modal + */ +export default function CustomStatus({ + ...props +}: ModalProps<"custom_status">) { + const client = useClient(); + + return ( + } + schema={{ + text: "text", + }} + defaults={{ + text: client.user?.status?.text as string, + }} + data={{ + text: { + field: ( + + ) as React.ReactChild, + }, + }} + callback={({ text }) => + client.users.edit({ + status: { + ...client.user?.status, + text: text.trim().length > 0 ? text : undefined, + }, + }) + } + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/DeleteMessage.tsx b/src/controllers/modals/components/DeleteMessage.tsx new file mode 100644 index 00000000..29f94746 --- /dev/null +++ b/src/controllers/modals/components/DeleteMessage.tsx @@ -0,0 +1,39 @@ +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import Message from "../../../components/common/messaging/Message"; +import { ModalProps } from "../types"; + +/** + * Delete message modal + */ +export default function DeleteMessage({ + target, + ...props +}: ModalProps<"delete_message">) { + return ( + } + description={ + + } + schema={{ + message: "custom", + }} + data={{ + message: { + element: , + }, + }} + callback={() => target.delete()} + submit={{ + palette: "error", + children: , + }} + /> + ); +} diff --git a/src/context/intermediate/modals/Error.tsx b/src/controllers/modals/components/Error.tsx similarity index 64% rename from src/context/intermediate/modals/Error.tsx rename to src/controllers/modals/components/Error.tsx index fd668fc6..892f5794 100644 --- a/src/context/intermediate/modals/Error.tsx +++ b/src/controllers/modals/components/Error.tsx @@ -1,25 +1,24 @@ import { Text } from "preact-i18n"; -import Modal from "../../../components/ui/Modal"; +import { Modal } from "@revoltchat/ui"; -interface Props { - onClose: () => void; - error: string; -} +import { noopTrue } from "../../../lib/js"; -export function ErrorModal({ onClose, error }: Props) { +import { ModalProps } from "../types"; + +export default function Error({ error, ...props }: ModalProps<"error">) { return ( } actions={[ { - onClick: onClose, + onClick: noopTrue, confirmation: true, children: , }, { + palette: "plain-secondary", onClick: () => location.reload(), children: , }, diff --git a/src/context/intermediate/popovers/ImageViewer.tsx b/src/controllers/modals/components/ImageViewer.tsx similarity index 56% rename from src/context/intermediate/popovers/ImageViewer.tsx rename to src/controllers/modals/components/ImageViewer.tsx index 3d51ff3e..526ec9c4 100644 --- a/src/context/intermediate/popovers/ImageViewer.tsx +++ b/src/controllers/modals/components/ImageViewer.tsx @@ -1,23 +1,40 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { API } from "revolt.js"; +import styled from "styled-components"; -import styles from "./ImageViewer.module.scss"; +import { Modal } from "@revoltchat/ui"; import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions"; import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions"; -import Modal from "../../../components/ui/Modal"; +import { useClient } from "../../client/ClientController"; +import { ModalProps } from "../types"; -import { useClient } from "../../revoltjs/RevoltClient"; +const Viewer = styled.div` + display: flex; + overflow: hidden; + flex-direction: column; + border-end-end-radius: 4px; + border-end-start-radius: 4px; -interface Props { - onClose: () => void; - embed?: API.Image; - attachment?: API.File; -} + max-width: 100vw; -type ImageMetadata = API.Metadata & { type: "Image" }; + img { + width: auto; + height: auto; + max-width: 90vw; + max-height: 75vh; + object-fit: contain; + border-bottom: thin solid var(--tertiary-foreground); + + -webkit-touch-callout: default; + } +`; + +export default function ImageViewer({ + embed, + attachment, + ...props +}: ModalProps<"image_viewer">) { + const client = useClient(); -export function ImageViewer({ attachment, embed, onClose }: Props) { if (attachment && attachment.metadata.type !== "Image") { console.warn( `Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`, @@ -25,20 +42,16 @@ export function ImageViewer({ attachment, embed, onClose }: Props) { return null; } - const client = useClient(); - return ( - -
+ + {attachment && ( <> @@ -54,7 +67,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) { )} -
+
); } diff --git a/src/controllers/modals/components/ImportTheme.tsx b/src/controllers/modals/components/ImportTheme.tsx new file mode 100644 index 00000000..b76c7634 --- /dev/null +++ b/src/controllers/modals/components/ImportTheme.tsx @@ -0,0 +1,35 @@ +import { Text } from "preact-i18n"; + +import { ModalForm } from "@revoltchat/ui"; + +import { state } from "../../../mobx/State"; + +import { ModalProps } from "../types"; + +/** + * Import theme modal + */ +export default function ImportTheme({ ...props }: ModalProps<"import_theme">) { + return ( + } + schema={{ + data: "text", + }} + data={{ + data: { + field: ( + + ) as React.ReactChild, + }, + }} + callback={async ({ data }) => + state.settings.theme.hydrate(JSON.parse(data)) + } + submit={{ + children: , + }} + /> + ); +} diff --git a/src/controllers/modals/components/KickMember.tsx b/src/controllers/modals/components/KickMember.tsx new file mode 100644 index 00000000..213850cc --- /dev/null +++ b/src/controllers/modals/components/KickMember.tsx @@ -0,0 +1,42 @@ +import { Text } from "preact-i18n"; + +import { Column, ModalForm } from "@revoltchat/ui"; + +import UserIcon from "../../../components/common/user/UserIcon"; +import { ModalProps } from "../types"; + +/** + * Kick member modal + */ +export default function KickMember({ + member, + ...props +}: ModalProps<"kick_member">) { + return ( + } + schema={{ + member: "custom", + }} + data={{ + member: { + element: ( + + + + + ), + }, + }} + callback={() => member.kick()} + submit={{ + palette: "error", + children: , + }} + /> + ); +} diff --git a/src/context/intermediate/modals/ExternalLinkPrompt.tsx b/src/controllers/modals/components/LinkWarning.tsx similarity index 61% rename from src/context/intermediate/modals/ExternalLinkPrompt.tsx rename to src/controllers/modals/components/LinkWarning.tsx index 09efd941..04a9e876 100644 --- a/src/context/intermediate/modals/ExternalLinkPrompt.tsx +++ b/src/controllers/modals/components/LinkWarning.tsx @@ -1,38 +1,33 @@ import { Text } from "preact-i18n"; +import { Modal } from "@revoltchat/ui"; + +import { noopTrue } from "../../../lib/js"; + import { useApplicationState } from "../../../mobx/State"; -import Modal from "../../../components/ui/Modal"; +import { ModalProps } from "../types"; -import { useIntermediate } from "../Intermediate"; - -interface Props { - onClose: () => void; - link: string; -} - -export function ExternalLinkModal({ onClose, link }: Props) { - const { openLink } = useIntermediate(); +export default function LinkWarning({ + link, + callback, + ...props +}: ModalProps<"link_warning">) { const settings = useApplicationState().settings; return ( } actions={[ { - onClick: () => { - openLink(link, true); - onClose(); - }, + onClick: callback, confirmation: true, - contrast: true, - accent: true, + palette: "accent", children: "Continue", }, { - onClick: onClose, + onClick: noopTrue, confirmation: false, children: "Cancel", }, @@ -43,10 +38,9 @@ export function ExternalLinkModal({ onClose, link }: Props) { settings.security.addTrustedOrigin(url.hostname); } catch (e) {} - openLink(link, true); - onClose(); + return callback(); }, - plain: true, + palette: "plain", children: ( ), diff --git a/src/controllers/modals/components/MFAEnableTOTP.tsx b/src/controllers/modals/components/MFAEnableTOTP.tsx new file mode 100644 index 00000000..32caa8fd --- /dev/null +++ b/src/controllers/modals/components/MFAEnableTOTP.tsx @@ -0,0 +1,93 @@ +import { QRCodeSVG } from "qrcode.react"; +import styled from "styled-components"; + +import { Text } from "preact-i18n"; +import { useState } from "preact/hooks"; + +import { Category, Centred, Column, InputBox, Modal } from "@revoltchat/ui"; + +import { ModalProps } from "../types"; + +const Code = styled.code` + user-select: all; +`; + +const Qr = styled.div` + border-radius: 4px; + background: white; + + width: 140px; + height: 140px; + + display: grid; + place-items: center; +`; + +/** + * TOTP enable modal + */ +export default function MFAEnableTOTP({ + identifier, + secret, + callback, + onClose, + signal, +}: ModalProps<"mfa_enable_totp">) { + const uri = `otpauth://totp/Revolt:${identifier}?secret=${secret}&issuer=Revolt`; + const [value, setValue] = useState(""); + + return ( + } + description={} + actions={[ + { + palette: "primary", + children: , + onClick: () => { + callback(value.trim().replace(/\s/g, "")); + return true; + }, + confirmation: true, + }, + { + palette: "plain", + children: , + onClick: () => { + callback(); + return true; + }, + }, + ]} + onClose={() => { + callback(); + onClose(); + }} + signal={signal} + nonDismissable> + + + + + + + + {secret} + + + + + + + + setValue(e.currentTarget.value)} + /> + + ); +} diff --git a/src/controllers/modals/components/MFAFlow.tsx b/src/controllers/modals/components/MFAFlow.tsx new file mode 100644 index 00000000..21bd2e0f --- /dev/null +++ b/src/controllers/modals/components/MFAFlow.tsx @@ -0,0 +1,225 @@ +import { Archive } from "@styled-icons/boxicons-regular"; +import { Key, Keyboard } from "@styled-icons/boxicons-solid"; +import { API } from "revolt.js"; + +import { Text } from "preact-i18n"; +import { + useCallback, + useEffect, + useLayoutEffect, + useState, +} from "preact/hooks"; + +import { + Category, + CategoryButton, + InputBox, + Modal, + Preloader, +} from "@revoltchat/ui"; + +import { ModalProps } from "../types"; + +/** + * Mapping of MFA methods to icons + */ +const ICONS: Record> = { + Password: Keyboard, + Totp: Key, + Recovery: Archive, +}; + +/** + * Component for handling challenge entry + */ +function ResponseEntry({ + type, + value, + onChange, +}: { + type: API.MFAMethod; + value?: API.MFAResponse; + onChange: (v: API.MFAResponse) => void; +}) { + return ( + <> + + + + + {type === "Password" && ( + + onChange({ password: e.currentTarget.value }) + } + /> + )} + + {type === "Totp" && ( + + onChange({ totp_code: e.currentTarget.value }) + } + /> + )} + + {type === "Recovery" && ( + + onChange({ recovery_code: e.currentTarget.value }) + } + /> + )} + + ); +} + +/** + * MFA ticket creation flow + */ +export default function MFAFlow({ + onClose, + signal, + ...props +}: ModalProps<"mfa_flow">) { + const [methods, setMethods] = useState( + props.state === "unknown" ? props.available_methods : undefined, + ); + + // Current state of the modal + const [selectedMethod, setSelected] = useState(); + const [response, setResponse] = useState(); + + // Fetch available methods if they have not been provided. + useEffect(() => { + if (!methods && props.state === "known") { + props.client.api.get("/auth/mfa/methods").then(setMethods); + } + }, []); + + // Always select first available method if only one available. + useLayoutEffect(() => { + if (methods && methods.length === 1) { + setSelected(methods[0]); + } + }, [methods]); + + // Callback to generate a new ticket or send response back up the chain. + const generateTicket = useCallback(async () => { + if (response) { + if (props.state === "known") { + const ticket = await props.client.api.put( + "/auth/mfa/ticket", + response, + ); + + props.callback(ticket); + } else { + props.callback(response); + } + + return true; + } + + return false; + }, [response]); + + return ( + } + description={ + + } + actions={ + selectedMethod + ? [ + { + palette: "primary", + children: ( + + ), + onClick: generateTicket, + confirmation: true, + }, + { + palette: "plain", + children: ( + + ), + onClick: () => { + if (methods!.length === 1) { + props.callback(); + return true; + } + setSelected(undefined); + }, + }, + ] + : [ + { + palette: "plain", + children: ( + + ), + onClick: () => { + props.callback(); + return true; + }, + }, + ] + } + // If we are logging in or have selected a method, + // don't allow the user to dismiss the modal by clicking off. + // This is to just generally prevent annoying situations + // where you accidentally close the modal while logging in + // or when switching to your password manager. + nonDismissable={ + props.state === "unknown" || + typeof selectedMethod !== "undefined" + } + signal={signal} + onClose={() => { + props.callback(); + onClose(); + }}> + {methods ? ( + selectedMethod ? ( + + ) : ( + methods.map((method) => { + const Icon = ICONS[method]; + return ( + } + onClick={() => setSelected(method)}> + + + ); + }) + ) + ) : ( + + )} + + ); +} diff --git a/src/controllers/modals/components/MFARecovery.tsx b/src/controllers/modals/components/MFARecovery.tsx new file mode 100644 index 00000000..33a11809 --- /dev/null +++ b/src/controllers/modals/components/MFARecovery.tsx @@ -0,0 +1,82 @@ +import styled from "styled-components"; + +import { Text } from "preact-i18n"; +import { useCallback, useState } from "preact/hooks"; + +import { Modal } from "@revoltchat/ui"; + +import { noopTrue } from "../../../lib/js"; + +import { toConfig } from "../../../components/settings/account/MultiFactorAuthentication"; +import { modalController } from "../ModalController"; +import { ModalProps } from "../types"; + +/** + * List of recovery codes + */ +const List = styled.div` + display: grid; + text-align: center; + grid-template-columns: 1fr 1fr; + font-family: var(--monospace-font), monospace; + + span { + user-select: text; + } +`; + +/** + * Recovery codes modal + */ +export default function MFARecovery({ + codes, + client, + onClose, + signal, +}: ModalProps<"mfa_recovery">) { + // Keep track of changes to recovery codes + const [known, setCodes] = useState(codes); + + // Subroutine to reset recovery codes + const reset = useCallback(async () => { + const ticket = await modalController.mfaFlow(client); + if (ticket) { + const codes = await client.api.patch( + "/auth/mfa/recovery", + undefined, + toConfig(ticket.token), + ); + + setCodes(codes); + } + + return false; + }, [client]); + + return ( + } + description={} + actions={[ + { + palette: "primary", + children: , + onClick: noopTrue, + confirmation: true, + }, + { + palette: "plain", + children: , + onClick: reset, + }, + ]} + onClose={onClose} + signal={signal}> + + {known.map((code) => ( + {code} + ))} + + + ); +} diff --git a/src/context/intermediate/popovers/ModifyAccount.tsx b/src/controllers/modals/components/ModifyAccount.tsx similarity index 81% rename from src/context/intermediate/popovers/ModifyAccount.tsx rename to src/controllers/modals/components/ModifyAccount.tsx index 20ec4dcc..8103647d 100644 --- a/src/context/intermediate/popovers/ModifyAccount.tsx +++ b/src/controllers/modals/components/ModifyAccount.tsx @@ -1,19 +1,16 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { Text } from "preact-i18n"; -import { useContext, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; -import Modal from "../../../components/ui/Modal"; -import Overline from "../../../components/ui/Overline"; +import { Category, Error, Modal } from "@revoltchat/ui"; + +import { noopTrue } from "../../../lib/js"; import FormField from "../../../pages/login/FormField"; -import { AppContext } from "../../revoltjs/RevoltClient"; -import { takeError } from "../../revoltjs/util"; - -interface Props { - onClose: () => void; - field: "username" | "email" | "password"; -} +import { useClient } from "../../client/ClientController"; +import { takeError } from "../../client/jsx/error"; +import { ModalProps } from "../types"; interface FormInputs { password: string; @@ -26,8 +23,11 @@ interface FormInputs { current_password?: string; } -export function ModifyAccountModal({ onClose, field }: Props) { - const client = useContext(AppContext); +export default function ModifyAccount({ + field, + ...props +}: ModalProps<"modify_account">) { + const client = useClient(); const [processing, setProcessing] = useState(false); const { handleSubmit, register, errors } = useForm(); const [error, setError] = useState(undefined); @@ -47,19 +47,19 @@ export function ModifyAccountModal({ onClose, field }: Props) { current_password: password, email: new_email, }); - onClose(); + props.onClose(); } else if (field === "password") { await client.api.patch("/auth/account/change/password", { current_password: password, password: new_password, }); - onClose(); + props.onClose(); } else if (field === "username") { await client.api.patch("/users/@me/username", { username: new_username, password, }); - onClose(); + props.onClose(); } } catch (err) { setError(takeError(err)); @@ -69,15 +69,13 @@ export function ModifyAccountModal({ onClose, field }: Props) { return ( } disabled={processing} actions={[ { - disabled: processing, confirmation: true, - onClick: handleSubmit(onSubmit), + onClick: () => void handleSubmit(onSubmit)(), children: field === "email" ? ( @@ -86,9 +84,9 @@ export function ModifyAccountModal({ onClose, field }: Props) { ), }, { - onClick: onClose, + onClick: noopTrue, children: , - plain: true, + palette: "plain", }, ]}> {/* Preact / React typing incompatabilities */} @@ -140,9 +138,13 @@ export function ModifyAccountModal({ onClose, field }: Props) { disabled={processing} /> {error && ( - - - + + + } + /> + )} diff --git a/src/controllers/modals/components/OutOfDate.tsx b/src/controllers/modals/components/OutOfDate.tsx new file mode 100644 index 00000000..fbbd58fa --- /dev/null +++ b/src/controllers/modals/components/OutOfDate.tsx @@ -0,0 +1,51 @@ +import { Text } from "preact-i18n"; + +import { Modal } from "@revoltchat/ui"; + +import { noop, noopTrue } from "../../../lib/js"; + +import { APP_VERSION } from "../../../version"; +import { ModalProps } from "../types"; + +/** + * Out-of-date indicator which instructs users + * that their client needs to be updated + */ +export default function OutOfDate({ + onClose, + version, +}: ModalProps<"out_of_date">) { + return ( + } + description={ + <> + +
+ + + } + actions={[ + { + palette: "plain", + onClick: noop, + children: ( + + ), + }, + { + palette: "plain-secondary", + onClick: noopTrue, + children: ( + + ), + }, + ]} + onClose={onClose} + nonDismissable + /> + ); +} diff --git a/src/controllers/modals/components/PendingFriendRequests.tsx b/src/controllers/modals/components/PendingFriendRequests.tsx new file mode 100644 index 00000000..2b9464d7 --- /dev/null +++ b/src/controllers/modals/components/PendingFriendRequests.tsx @@ -0,0 +1,21 @@ +import { Text } from "preact-i18n"; + +import { Column, Modal } from "@revoltchat/ui"; + +import { Friend } from "../../../pages/friends/Friend"; +import { ModalProps } from "../types"; + +export default function PendingFriendRequests({ + users, + ...props +}: ModalProps<"pending_friend_requests">) { + return ( + }> + + {users.map((x) => ( + + ))} + + + ); +} diff --git a/src/controllers/modals/components/ServerIdentity.tsx b/src/controllers/modals/components/ServerIdentity.tsx new file mode 100644 index 00000000..508aadae --- /dev/null +++ b/src/controllers/modals/components/ServerIdentity.tsx @@ -0,0 +1,138 @@ +import { X } from "@styled-icons/boxicons-regular"; +import { Save } from "@styled-icons/boxicons-solid"; +import { observer } from "mobx-react-lite"; +import styled from "styled-components"; + +import { Text } from "preact-i18n"; +import { useMemo, useState } from "preact/hooks"; + +import { + Button, + Category, + Centred, + Column, + InputBox, + Modal, + Row, + Message, +} from "@revoltchat/ui"; + +import { noop } from "../../../lib/js"; + +import { FileUploader } from "../../client/jsx/legacy/FileUploads"; +import { ModalProps } from "../types"; + +const Preview = styled(Centred)` + flex-grow: 1; + border-radius: var(--border-radius); + background: var(--secondary-background); + + > div { + padding: 0; + } +`; + +export default observer( + ({ member, ...props }: ModalProps<"server_identity">) => { + const [nickname, setNickname] = useState(member.nickname ?? ""); + + const message: any = useMemo(() => { + return { + author: member.user!, + member: { + ...member, + nickname, + }, + }; + }, []); + + return ( + + }> + + + + + + + + setNickname(e.currentTarget.value) + } + /> + + + + + + + + + + + member.edit({ avatar }).then(noop) + } + remove={() => + member + .edit({ remove: ["Avatar"] }) + .then(noop) + } + defaultPreview={member.user?.generateAvatarURL( + { + max_side: 256, + }, + false, + )} + previewURL={member.client.generateFileURL( + member.avatar ?? undefined, + { max_side: 256 }, + true, + )} + desaturateDefault + /> + + + Preview + + + + + + + + ); + }, +); diff --git a/src/controllers/modals/components/ServerInfo.tsx b/src/controllers/modals/components/ServerInfo.tsx new file mode 100644 index 00000000..786026ad --- /dev/null +++ b/src/controllers/modals/components/ServerInfo.tsx @@ -0,0 +1,48 @@ +import { X } from "@styled-icons/boxicons-regular"; + +import { Text } from "preact-i18n"; + +import { Column, H1, IconButton, Modal, Row } from "@revoltchat/ui"; + +import Markdown from "../../../components/markdown/Markdown"; +import { report } from "../../safety"; +import { modalController } from "../ModalController"; +import { ModalProps } from "../types"; + +export default function ServerInfo({ + server, + ...props +}: ModalProps<"server_info">) { + return ( + + +

{server.name}

+
+ + + + + } + actions={[ + { + onClick: () => + modalController.push({ + type: "server_identity", + member: server.member!, + }), + children: "Edit Identity", + palette: "primary", + }, + { + onClick: () => report(server), + children: , + palette: "error", + }, + ]}> + +
+ ); +} diff --git a/src/controllers/modals/components/ShowToken.tsx b/src/controllers/modals/components/ShowToken.tsx new file mode 100644 index 00000000..c28ca841 --- /dev/null +++ b/src/controllers/modals/components/ShowToken.tsx @@ -0,0 +1,35 @@ +import { Text } from "preact-i18n"; + +import { Modal } from "@revoltchat/ui"; + +import { noopTrue } from "../../../lib/js"; + +import { ModalProps } from "../types"; + +export default function ShowToken({ + name, + token, + ...props +}: ModalProps<"show_token">) { + return ( + + } + actions={[ + { + onClick: noopTrue, + confirmation: true, + children: , + }, + ]}> + + {token} + + + ); +} diff --git a/src/controllers/modals/components/SignOutSessions.tsx b/src/controllers/modals/components/SignOutSessions.tsx new file mode 100644 index 00000000..35b07882 --- /dev/null +++ b/src/controllers/modals/components/SignOutSessions.tsx @@ -0,0 +1,42 @@ +import { Text } from "preact-i18n"; +import { useCallback } from "preact/hooks"; + +import { Modal } from "@revoltchat/ui"; + +import { noopTrue } from "../../../lib/js"; + +import { ModalProps } from "../types"; + +/** + * Confirm whether a user wants to sign out of all other sessions + */ +export default function SignOutSessions( + props: ModalProps<"sign_out_sessions">, +) { + const onClick = useCallback(() => { + props.onDeleting(); + props.client.api.delete("/auth/session/all").then(props.onDelete); + return true; + }, []); + + return ( + } + actions={[ + { + onClick: noopTrue, + palette: "accent", + confirmation: true, + children: , + }, + { + onClick, + confirmation: true, + children: , + }, + ]}> +
+
+ ); +} diff --git a/src/context/intermediate/modals/SignedOut.tsx b/src/controllers/modals/components/SignedOut.tsx similarity index 51% rename from src/context/intermediate/modals/SignedOut.tsx rename to src/controllers/modals/components/SignedOut.tsx index 7b4979f6..0f3fa66d 100644 --- a/src/context/intermediate/modals/SignedOut.tsx +++ b/src/controllers/modals/components/SignedOut.tsx @@ -1,20 +1,22 @@ import { Text } from "preact-i18n"; -import Modal from "../../../components/ui/Modal"; +import { Modal } from "@revoltchat/ui"; -interface Props { - onClose: () => void; -} +import { noopTrue } from "../../../lib/js"; -export function SignedOutModal({ onClose }: Props) { +import { ModalProps } from "../types"; + +/** + * Indicate that the user has been signed out of their account + */ +export default function SignedOut(props: ModalProps<"signed_out">) { return ( } actions={[ { - onClick: onClose, + onClick: noopTrue, confirmation: true, children: , }, diff --git a/src/context/intermediate/popovers/UserPicker.tsx b/src/controllers/modals/components/UserPicker.tsx similarity index 62% rename from src/context/intermediate/popovers/UserPicker.tsx rename to src/controllers/modals/components/UserPicker.tsx index c9be5fab..86727a00 100644 --- a/src/context/intermediate/popovers/UserPicker.tsx +++ b/src/controllers/modals/components/UserPicker.tsx @@ -1,48 +1,56 @@ -import styles from "./UserPicker.module.scss"; +import styled from "styled-components"; + import { Text } from "preact-i18n"; -import { useState } from "preact/hooks"; +import { useMemo, useState } from "preact/hooks"; + +import { Modal } from "@revoltchat/ui"; import UserCheckbox from "../../../components/common/user/UserCheckbox"; -import Modal from "../../../components/ui/Modal"; +import { useClient } from "../../client/ClientController"; +import { ModalProps } from "../types"; -import { useClient } from "../../revoltjs/RevoltClient"; +const List = styled.div` + max-width: 100%; + max-height: 360px; + overflow-y: scroll; +`; -interface Props { - omit?: string[]; - onClose: () => void; - callback: (users: string[]) => Promise; -} - -export function UserPicker(props: Props) { +export default function UserPicker({ + callback, + omit, + ...props +}: ModalProps<"user_picker">) { const [selected, setSelected] = useState([]); - const omit = [...(props.omit || []), "00000000000000000000000000"]; + const omitted = useMemo( + () => new Set([...(omit || []), "00000000000000000000000000"]), + [omit], + ); const client = useClient(); return ( } - onClose={props.onClose} actions={[ { children: , - onClick: () => props.callback(selected).then(props.onClose), + onClick: () => callback(selected).then(() => true), }, ]}> -
+ {[...client.users.values()] .filter( (x) => x && x.relationship === "Friend" && - !omit.includes(x._id), + !omitted.has(x._id), ) .map((x) => ( { if (v) { setSelected([...selected, x._id]); @@ -54,7 +62,7 @@ export function UserPicker(props: Props) { }} /> ))} -
+
); } diff --git a/src/context/intermediate/modals/Onboarding.module.scss b/src/controllers/modals/components/legacy/Onboarding.module.scss similarity index 87% rename from src/context/intermediate/modals/Onboarding.module.scss rename to src/controllers/modals/components/legacy/Onboarding.module.scss index 887b4224..42ebfbdd 100644 --- a/src/context/intermediate/modals/Onboarding.module.scss +++ b/src/controllers/modals/components/legacy/Onboarding.module.scss @@ -1,5 +1,12 @@ .onboarding { - height: 100vh; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: var(--background); + display: flex; align-items: center; flex-direction: column; diff --git a/src/context/intermediate/modals/Onboarding.tsx b/src/controllers/modals/components/legacy/Onboarding.tsx similarity index 84% rename from src/context/intermediate/modals/Onboarding.tsx rename to src/controllers/modals/components/legacy/Onboarding.tsx index a6055791..bc3f22c3 100644 --- a/src/context/intermediate/modals/Onboarding.tsx +++ b/src/controllers/modals/components/legacy/Onboarding.tsx @@ -4,23 +4,22 @@ import styles from "./Onboarding.module.scss"; import { Text } from "preact-i18n"; import { useState } from "preact/hooks"; -import Button from "../../../components/ui/Button"; -import Preloader from "../../../components/ui/Preloader"; +import { Button, Preloader } from "@revoltchat/ui"; + import wideSVG from "/assets/wide.svg"; -import FormField from "../../../pages/login/FormField"; -import { takeError } from "../../revoltjs/util"; - -interface Props { - onClose: () => void; - callback: (username: string, loginAfterSuccess?: true) => Promise; -} +import FormField from "../../../../pages/login/FormField"; +import { takeError } from "../../../client/jsx/error"; +import { ModalProps } from "../../types"; interface FormInputs { username: string; } -export function OnboardingModal({ onClose, callback }: Props) { +export function OnboardingModal({ + callback, + ...props +}: ModalProps<"onboarding">) { const { handleSubmit, register } = useForm(); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); @@ -28,7 +27,7 @@ export function OnboardingModal({ onClose, callback }: Props) { const onSubmit: SubmitHandler = ({ username }) => { setLoading(true); callback(username, true) - .then(() => onClose()) + .then(() => props.onClose()) .catch((err: unknown) => { setError(takeError(err)); setLoading(false); diff --git a/src/context/intermediate/popovers/UserProfile.module.scss b/src/controllers/modals/components/legacy/UserProfile.module.scss similarity index 100% rename from src/context/intermediate/popovers/UserProfile.module.scss rename to src/controllers/modals/components/legacy/UserProfile.module.scss diff --git a/src/context/intermediate/popovers/UserProfile.tsx b/src/controllers/modals/components/legacy/UserProfile.tsx similarity index 84% rename from src/context/intermediate/popovers/UserProfile.tsx rename to src/controllers/modals/components/legacy/UserProfile.tsx index c2b6b521..a7cc7f8e 100644 --- a/src/context/intermediate/popovers/UserProfile.tsx +++ b/src/controllers/modals/components/legacy/UserProfile.tsx @@ -13,42 +13,38 @@ import { UserPermission, API } from "revolt.js"; import styles from "./UserProfile.module.scss"; import { Localizer, Text } from "preact-i18n"; -import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks"; +import { useEffect, useLayoutEffect, useState } from "preact/hooks"; -import { noop } from "../../../lib/js"; - -import ChannelIcon from "../../../components/common/ChannelIcon"; -import ServerIcon from "../../../components/common/ServerIcon"; -import Tooltip from "../../../components/common/Tooltip"; -import UserBadges from "../../../components/common/user/UserBadges"; -import UserIcon from "../../../components/common/user/UserIcon"; -import { Username } from "../../../components/common/user/UserShort"; -import UserStatus from "../../../components/common/user/UserStatus"; -import Button from "../../../components/ui/Button"; -import IconButton from "../../../components/ui/IconButton"; -import Modal from "../../../components/ui/Modal"; -import Overline from "../../../components/ui/Overline"; -import Preloader from "../../../components/ui/Preloader"; - -import Markdown from "../../../components/markdown/Markdown"; import { - ClientStatus, - StatusContext, - useClient, -} from "../../revoltjs/RevoltClient"; -import { useIntermediate } from "../Intermediate"; + Button, + Category, + Error, + IconButton, + Modal, + Preloader, +} from "@revoltchat/ui"; -interface Props { - user_id: string; - dummy?: boolean; - onClose?: () => void; - dummyProfile?: API.UserProfile; -} +import { noop } from "../../../../lib/js"; + +import ChannelIcon from "../../../../components/common/ChannelIcon"; +import ServerIcon from "../../../../components/common/ServerIcon"; +import Tooltip from "../../../../components/common/Tooltip"; +import UserBadges from "../../../../components/common/user/UserBadges"; +import UserIcon from "../../../../components/common/user/UserIcon"; +import { Username } from "../../../../components/common/user/UserShort"; +import UserStatus from "../../../../components/common/user/UserStatus"; +import Markdown from "../../../../components/markdown/Markdown"; +import { useSession } from "../../../../controllers/client/ClientController"; +import { modalController } from "../../../../controllers/modals/ModalController"; +import { ModalProps } from "../../types"; export const UserProfile = observer( - ({ user_id, onClose, dummy, dummyProfile }: Props) => { - const { openScreen, writeClipboard } = useIntermediate(); - + ({ + user_id, + dummy, + dummyProfile, + ...props + }: ModalProps<"user_profile">) => { const [profile, setProfile] = useState< undefined | null | API.UserProfile >(undefined); @@ -60,13 +56,13 @@ export const UserProfile = observer( >(); const history = useHistory(); - const client = useClient(); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; const [tab, setTab] = useState("profile"); const user = client.users.get(user_id); if (!user) { - if (onClose) useEffect(onClose, []); + if (props.onClose) useEffect(props.onClose, []); return null; } @@ -98,32 +94,26 @@ export const UserProfile = observer( useEffect(() => { if (dummy) return; - if ( - status === ClientStatus.ONLINE && - typeof mutual === "undefined" - ) { + if (session.state === "Online" && typeof mutual === "undefined") { setMutual(null); user.fetchMutual().then(setMutual); } - }, [mutual, status, dummy, user]); + }, [mutual, session.state, dummy, user]); useEffect(() => { if (dummy) return; - if ( - status === ClientStatus.ONLINE && - typeof profile === "undefined" - ) { + if (session.state === "Online" && typeof profile === "undefined") { setProfile(null); if (user.permission & UserPermission.ViewProfile) { user.fetchProfile().then(setProfile).catch(noop); } } - }, [profile, status, dummy, user]); + }, [profile, session.state, dummy, user]); useEffect(() => { if ( - status === ClientStatus.ONLINE && + session.state === "Online" && user.bot && typeof isPublicBot === "undefined" ) { @@ -133,7 +123,7 @@ export const UserProfile = observer( .then(() => setIsPublicBot(true)) .catch(noop); } - }, [isPublicBot, status, user, client.bots]); + }, [isPublicBot, session.state, user, client.bots]); const backgroundURL = profile && @@ -146,13 +136,8 @@ export const UserProfile = observer( const badges = user.badges ?? 0; const flags = user.flags ?? 0; - return ( - + const children = ( + <>
user.avatar && - openScreen({ - id: "image_viewer", + modalController.push({ + type: "image_viewer", attachment: user.avatar, }) } @@ -182,7 +167,7 @@ export const UserProfile = observer( - writeClipboard(user.username) + modalController.writeText(user.username) }> @{user.username} @@ -195,7 +180,10 @@ export const UserProfile = observer(
{isPublicBot && ( - @@ -208,7 +196,7 @@ export const UserProfile = observer( }> { - onClose?.(); + props.onClose?.(); history.push(`/open/${user_id}`); }}> @@ -219,7 +207,7 @@ export const UserProfile = observer( {user.relationship === "User" && !dummy && ( { - onClose?.(); + props.onClose?.(); history.push(`/settings/profile`); }}> @@ -277,19 +265,19 @@ export const UserProfile = observer(
{flags & 1 ? ( /** ! FIXME: i18n this area */ - - User is suspended - + + + ) : undefined} {flags & 2 ? ( - - User deleted their account - + + + ) : undefined} {flags & 4 ? ( - - User is banned - + + + ) : undefined} {user.bot ? ( <> @@ -299,8 +287,8 @@ export const UserProfile = observer(
user.bot && - openScreen({ - id: "profile", + modalController.push({ + type: "user_profile", user_id: user.bot.owner, }) } @@ -363,8 +351,8 @@ export const UserProfile = observer( x && (
- openScreen({ - id: "profile", + modalController.push({ + type: "user_profile", user_id: x._id, }) } @@ -437,6 +425,18 @@ export const UserProfile = observer(
))}
+ + ); + + if (dummy) return
{children}
; + + return ( + + {children} ); }, diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts new file mode 100644 index 00000000..1275fe4c --- /dev/null +++ b/src/controllers/modals/types.ts @@ -0,0 +1,187 @@ +import { API, Client, User, Member, Channel, Server, Message } from "revolt.js"; + +export type Modal = { + key?: string; +} & ( + | { + type: + | "signed_out" + | "create_group" + | "create_server" + | "custom_status" + | "add_friend"; + } + | ({ + type: "mfa_flow"; + } & ( + | { + state: "known"; + client: Client; + callback: (ticket?: API.MFATicket) => void; + } + | { + state: "unknown"; + available_methods: API.MFAMethod[]; + callback: (response?: API.MFAResponse) => void; + } + )) + | { type: "mfa_recovery"; codes: string[]; client: Client } + | { + type: "mfa_enable_totp"; + identifier: string; + secret: string; + callback: (code?: string) => void; + } + | { + type: "out_of_date"; + version: string; + } + | { + type: "changelog"; + initial?: number; + } + | { + type: "sign_out_sessions"; + client: Client; + onDelete: () => void; + onDeleting: () => void; + } + | { + type: "show_token"; + name: string; + token: string; + } + | { + type: "error"; + error: string; + } + | { + type: "clipboard"; + text: string; + } + | { + type: "link_warning"; + link: string; + callback: () => true; + } + | { + type: "pending_friend_requests"; + users: User[]; + } + | { + type: "modify_account"; + client: Client; + field: "username" | "email" | "password"; + } + | { + type: "server_identity"; + member: Member; + } + | { + type: "channel_info"; + channel: Channel; + } + | { + type: "server_info"; + server: Server; + } + | { + type: "image_viewer"; + embed?: API.Image; + attachment?: API.File; + } + | { + type: "user_picker"; + omit?: string[]; + callback: (users: string[]) => Promise; + } + | { + type: "user_profile"; + user_id: string; + dummy?: boolean; + dummyProfile?: API.UserProfile; + } + | { + type: "create_bot"; + onCreate: (bot: API.Bot) => void; + } + | { + type: "onboarding"; + callback: ( + username: string, + loginAfterSuccess?: true, + ) => Promise; + } + | { + type: "create_role"; + server: Server; + callback: (id: string) => void; + } + | { + type: "leave_group"; + target: Channel; + } + | { + type: "close_dm"; + target: Channel; + } + | { + type: "delete_channel"; + target: Channel; + } + | { + type: "create_invite"; + target: Channel; + } + | { + type: "leave_server"; + target: Server; + } + | { + type: "delete_server"; + target: Server; + } + | { + type: "delete_bot"; + target: string; + name: string; + cb?: () => void; + } + | { + type: "delete_message"; + target: Message; + } + | { + type: "kick_member"; + member: Member; + } + | { + type: "ban_member"; + member: Member; + } + | { + type: "unfriend_user"; + target: User; + } + | { + type: "block_user"; + target: User; + } + | { + type: "create_channel"; + target: Server; + cb?: (channel: Channel) => void; + } + | { + type: "create_category"; + target: Server; + } + | { + type: "import_theme"; + } +); + +export type ModalProps = Modal & { type: T } & { + onClose: () => void; + signal?: "close" | "confirm"; +}; diff --git a/src/controllers/safety/index.ts b/src/controllers/safety/index.ts new file mode 100644 index 00000000..bfcb2471 --- /dev/null +++ b/src/controllers/safety/index.ts @@ -0,0 +1,16 @@ +import { Server } from "revolt.js"; + +export function report(object: Server) { + let type; + if (object instanceof Server) { + type = "Server"; + } + + window.open( + `mailto:abuse@revolt.chat?subject=${encodeURIComponent( + `${type} Report`, + )}&body=${encodeURIComponent( + `${type} ID: ${object._id}\nWrite more information here!`, + )}`, + ); +} diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 2e38dfa9..90e3869c 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -1,20 +1,17 @@ -import { - At, - Bell, - BellOff, - Check, - CheckSquare, - ChevronRight, - Block, - Square, - LeftArrowAlt, - Trash, -} from "@styled-icons/boxicons-regular"; +import { ChevronRight, Trash } from "@styled-icons/boxicons-regular"; import { Cog, UserVoice } from "@styled-icons/boxicons-solid"; import { isFirefox } from "react-device-detect"; import { useHistory } from "react-router-dom"; -import { Channel, Message, Server, User, API } from "revolt.js"; -import { Permission, UserPermission } from "revolt.js"; +import { + Channel, + Message, + Server, + User, + API, + Permission, + UserPermission, + Member, +} from "revolt.js"; import { ContextMenuWithData, @@ -22,27 +19,20 @@ import { openContextMenu, } from "preact-context-menu"; import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; + +import { IconButton, LineDivider } from "@revoltchat/ui"; import { useApplicationState } from "../mobx/State"; import { QueuedMessage } from "../mobx/stores/MessageQueue"; import { NotificationState } from "../mobx/stores/NotificationOptions"; -import { Screen, useIntermediate } from "../context/intermediate/Intermediate"; -import { - AppContext, - ClientStatus, - StatusContext, -} from "../context/revoltjs/RevoltClient"; -import { takeError } from "../context/revoltjs/util"; import CMNotifications from "./contextmenu/CMNotifications"; import Tooltip from "../components/common/Tooltip"; import UserStatus from "../components/common/user/UserStatus"; -import IconButton from "../components/ui/IconButton"; -import LineDivider from "../components/ui/LineDivider"; - -import { Children } from "../types/Preact"; +import { useSession } from "../controllers/client/ClientController"; +import { takeError } from "../controllers/client/jsx/error"; +import { modalController } from "../controllers/modals/ModalController"; import { internalEmit } from "./eventEmitter"; import { getRenderer } from "./renderer/Singleton"; @@ -80,8 +70,8 @@ type Action = | { action: "open_link"; link: string } | { action: "copy_link"; link: string } | { action: "remove_member"; channel: Channel; user: User } - | { action: "kick_member"; target: Server; user: User } - | { action: "ban_member"; target: Server; user: User } + | { action: "kick_member"; target: Member } + | { action: "ban_member"; target: Member } | { action: "view_profile"; user: User } | { action: "message_user"; user: User } | { action: "block_user"; user: User } @@ -106,7 +96,7 @@ type Action = | { action: "close_dm"; target: Channel } | { action: "leave_server"; target: Server } | { action: "delete_server"; target: Server } - | { action: "edit_identity"; target: Server } + | { action: "edit_identity"; target: Member } | { action: "open_notification_options"; channel?: Channel; @@ -125,13 +115,12 @@ type Action = // ! FIXME: I dare someone to re-write this // Tip: This should just be split into separate context menus per logical area. export default function ContextMenus() { - const { openScreen, writeClipboard } = useIntermediate(); - const client = useContext(AppContext); + const session = useSession()!; + const client = session.client!; const userId = client.user!._id; - const status = useContext(StatusContext); - const isOnline = status === ClientStatus.ONLINE; const state = useApplicationState(); const history = useHistory(); + const isOnline = session.state === "Online"; function contextClick(data?: Action) { if (typeof data === "undefined") return; @@ -139,7 +128,7 @@ export default function ContextMenus() { (async () => { switch (data.action) { case "copy_id": - writeClipboard(data.id); + modalController.writeText(data.id); break; case "copy_message_link": { @@ -148,11 +137,13 @@ export default function ContextMenus() { if (channel?.channel_type === "TextChannel") pathname = `/server/${channel.server_id}${pathname}`; - writeClipboard(window.origin + pathname); + modalController.writeText(window.origin + pathname); } break; case "copy_selection": - writeClipboard(document.getSelection()?.toString() ?? ""); + modalController.writeText( + document.getSelection()?.toString() ?? "", + ); break; case "mark_as_read": { @@ -209,7 +200,7 @@ export default function ContextMenus() { .get(data.message.channel)! .sendMessage({ nonce: data.message.id, - content: data.message.data.content as string, + content: data.message.data.content, replies: data.message.data.replies, }) .catch(fail); @@ -236,7 +227,7 @@ export default function ContextMenus() { break; case "copy_text": - writeClipboard(data.content); + modalController.writeText(data.content); break; case "reply_message": @@ -295,7 +286,7 @@ export default function ContextMenus() { case "copy_file_link": { const { filename } = data.attachment; - writeClipboard( + modalController.writeText( // ! FIXME: do from r.js `${client.generateFileURL( data.attachment, @@ -312,7 +303,7 @@ export default function ContextMenus() { case "copy_link": { - writeClipboard(data.link); + modalController.writeText(data.link); } break; @@ -323,7 +314,10 @@ export default function ContextMenus() { break; case "view_profile": - openScreen({ id: "profile", user_id: data.user._id }); + modalController.push({ + type: "user_profile", + user_id: data.user._id, + }); break; case "message_user": @@ -342,8 +336,7 @@ export default function ContextMenus() { break; case "block_user": - openScreen({ - id: "special_prompt", + modalController.push({ type: "block_user", target: data.user, }); @@ -352,8 +345,7 @@ export default function ContextMenus() { await data.user.unblockUser(); break; case "remove_friend": - openScreen({ - id: "special_prompt", + modalController.push({ type: "unfriend_user", target: data.user, }); @@ -374,51 +366,54 @@ export default function ContextMenus() { break; case "set_status": - openScreen({ - id: "special_input", - type: "set_custom_status", + modalController.push({ + type: "custom_status", }); break; case "clear_status": - { - const { text: _text, ...status } = - client.user?.status ?? {}; - await client.users.edit({ status }); - } + await client.users.edit({ remove: ["StatusText"] }); + break; + + case "delete_message": + modalController.push({ + type: "delete_message", + target: data.target, + }); break; case "leave_group": case "close_dm": - case "leave_server": case "delete_channel": - case "delete_server": - case "delete_message": - case "create_channel": - case "create_category": case "create_invite": - // Typescript flattens the case types into a single type and type structure and specifity is lost - openScreen({ - id: "special_prompt", + modalController.push({ type: data.action, target: data.target, - } as unknown as Screen); + }); + break; + + case "leave_server": + case "delete_server": + case "create_channel": + case "create_category": + modalController.push({ + type: data.action, + target: data.target, + }); break; case "edit_identity": - openScreen({ - id: "server_identity", - server: data.target, + modalController.push({ + type: "server_identity", + member: data.target, }); break; case "ban_member": case "kick_member": - openScreen({ - id: "special_prompt", + modalController.push({ type: data.action, - target: data.target, - user: data.user, + member: data.target, }); break; @@ -446,7 +441,10 @@ export default function ContextMenus() { break; } })().catch((err) => { - openScreen({ id: "error", error: takeError(err) }); + modalController.push({ + type: "error", + error: takeError(err), + }); }); } @@ -492,7 +490,7 @@ export default function ContextMenus() { function pushDivider() { if (lastDivider || elements.length === 0) return; lastDivider = true; - elements.push(); + elements.push(); } if (server_list) { @@ -641,7 +639,7 @@ export default function ContextMenus() { } for (let i = 0; i < actions.length; i++) { - let action = actions[i]; + const action = actions[i]; if (action) { generateAction({ action, @@ -671,31 +669,36 @@ export default function ContextMenus() { userId !== uid && uid !== server.owner ) { - if (serverPermissions & Permission.KickMembers) - generateAction( - { - action: "kick_member", - target: server, - user: user!, - }, - undefined, // this is needed because generateAction uses positional, not named parameters - undefined, - null, - "var(--error)", // the only relevant part really - ); + const member = client.members.getKey({ + server: server._id, + user: user!._id, + })!; - if (serverPermissions & Permission.BanMembers) - generateAction( - { - action: "ban_member", - target: server, - user: user!, - }, - undefined, - undefined, - null, - "var(--error)", - ); + if (member) { + if (serverPermissions & Permission.KickMembers) + generateAction( + { + action: "kick_member", + target: member, + }, + undefined, // this is needed because generateAction uses positional, not named parameters + undefined, + null, + "var(--error)", // the only relevant part really + ); + + if (serverPermissions & Permission.BanMembers) + generateAction( + { + action: "ban_member", + target: member, + }, + undefined, + undefined, + null, + "var(--error)", + ); + } } } @@ -956,7 +959,10 @@ export default function ContextMenus() { serverPermissions & Permission.ChangeAvatar ) generateAction( - { action: "edit_identity", target: server }, + { + action: "edit_identity", + target: server.member!, + }, "edit_identity", ); @@ -1017,7 +1023,7 @@ export default function ContextMenus() {
- writeClipboard( + modalController.writeText( client.user!.username, ) }> @@ -1045,7 +1051,7 @@ export default function ContextMenus() {
- + - + diff --git a/src/lib/ErrorBoundary.tsx b/src/lib/ErrorBoundary.tsx index 10942d54..f755b788 100644 --- a/src/lib/ErrorBoundary.tsx +++ b/src/lib/ErrorBoundary.tsx @@ -6,7 +6,6 @@ import styled from "styled-components/macro"; import { useEffect, useErrorBoundary, useState } from "preact/hooks"; import { GIT_REVISION } from "../revision"; -import { Children } from "../types/Preact"; const CrashContainer = styled.div` height: 100%; diff --git a/src/lib/FakeClient.tsx b/src/lib/FakeClient.tsx deleted file mode 100644 index d2523ef5..00000000 --- a/src/lib/FakeClient.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { observer } from "mobx-react-lite"; - -import { useMemo } from "preact/hooks"; - -import { useApplicationState } from "../mobx/State"; - -import { AppContext } from "../context/revoltjs/RevoltClient"; - -import { Children } from "../types/Preact"; - -export default observer(({ children }: { children: Children }) => { - const config = useApplicationState().config; - const client = useMemo(() => config.createClient(), [config.get()]); - return {children}; -}); diff --git a/src/lib/TextAreaAutoSize.tsx b/src/lib/TextAreaAutoSize.tsx index 51ae17b8..805d1145 100644 --- a/src/lib/TextAreaAutoSize.tsx +++ b/src/lib/TextAreaAutoSize.tsx @@ -3,7 +3,8 @@ import styled from "styled-components/macro"; import { RefObject } from "preact"; import { useEffect, useLayoutEffect, useRef } from "preact/hooks"; -import TextArea, { TextAreaProps } from "../components/ui/TextArea"; +import { TextArea } from "@revoltchat/ui"; +import type { TextAreaProps } from "@revoltchat/ui/esm/components/design/atoms/inputs/TextArea"; import { internalSubscribe } from "./eventEmitter"; import { isTouchscreenDevice } from "./isTouchscreenDevice"; diff --git a/src/lib/contextmenu/CMNotifications.tsx b/src/lib/contextmenu/CMNotifications.tsx index 0c235ede..72c4197d 100644 --- a/src/lib/contextmenu/CMNotifications.tsx +++ b/src/lib/contextmenu/CMNotifications.tsx @@ -15,13 +15,11 @@ import { Server } from "revolt.js"; import { ContextMenuWithData, MenuItem } from "preact-context-menu"; import { Text } from "preact-i18n"; +import { LineDivider } from "@revoltchat/ui"; + import { useApplicationState } from "../../mobx/State"; import { NotificationState } from "../../mobx/stores/NotificationOptions"; -import LineDivider from "../../components/ui/LineDivider"; - -import { Children } from "../../types/Preact"; - interface Action { key: string; type: "channel" | "server"; diff --git a/src/lib/conversion.ts b/src/lib/conversion.ts index f0840bb0..14469ea8 100644 --- a/src/lib/conversion.ts +++ b/src/lib/conversion.ts @@ -11,7 +11,7 @@ export function urlBase64ToUint8Array(base64String: string) { export function mapToRecord( map: Map, ) { - let record = {} as Record; + const record = {} as Record; map.forEach((v, k) => (record[k] = v)); return record; } diff --git a/src/lib/eventEmitter.ts b/src/lib/eventEmitter.ts index 1ad13148..c54460a9 100644 --- a/src/lib/eventEmitter.ts +++ b/src/lib/eventEmitter.ts @@ -31,3 +31,4 @@ export function internalEmit(ns: string, event: string, ...args: unknown[]) { // - PWA/update // - NewMessages/hide // - NewMessages/mark +// - System/alert diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx index 047e2bb7..41826d94 100644 --- a/src/lib/i18n.tsx +++ b/src/lib/i18n.tsx @@ -3,8 +3,6 @@ import { useContext } from "preact/hooks"; import { Dictionary } from "../context/Locale"; -import { Children } from "../types/Preact"; - interface Fields { [key: string]: Children; } diff --git a/src/lib/js.ts b/src/lib/js.ts index 80158291..5a35a5a2 100644 --- a/src/lib/js.ts +++ b/src/lib/js.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-empty-function */ export const noop = () => {}; export const noopAsync = async () => {}; +export const noopTrue = () => true; /* eslint-enable @typescript-eslint/no-empty-function */ diff --git a/src/lib/links.ts b/src/lib/links.ts index 15a696e4..2a3801fe 100644 --- a/src/lib/links.ts +++ b/src/lib/links.ts @@ -1,11 +1,7 @@ type LinkType = - | { type: "profile"; id: string } - | { type: "navigate"; path: string; navigation_type?: null } | { type: "navigate"; path: string; - navigation_type: "channel"; - channel_id: string; } | { type: "external"; href: string; url: URL } | { type: "none" }; @@ -17,9 +13,6 @@ const ALLOWED_ORIGINS = [ "local.revolt.chat", ]; -const CHANNEL_PATH_RE = - /^\/server\/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}\/channel\/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; - export function determineLink(href?: string): LinkType { let internal, url: URL | null = null; @@ -30,29 +23,12 @@ export function determineLink(href?: string): LinkType { if (ALLOWED_ORIGINS.includes(url.hostname)) { const path = url.pathname; - if (path.startsWith("/@")) { - const id = path.substr(2); - if (/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/.test(id)) { - return { type: "profile", id }; - } - } else { - if (CHANNEL_PATH_RE.test(path)) { - return { - type: "navigate", - path, - navigation_type: "channel", - channel_id: path.slice(43), - }; - } - return { type: "navigate", path }; - } - - internal = true; + return { type: "navigate", path }; } } catch (err) {} if (!internal && url) { - if (url.protocol !== "javascript") { + if (!url.protocol.startsWith("javascript")) { return { type: "external", href, url }; } } diff --git a/src/lib/renderer/Singleton.ts b/src/lib/renderer/Singleton.ts index f30b9ec0..b59e7a4c 100644 --- a/src/lib/renderer/Singleton.ts +++ b/src/lib/renderer/Singleton.ts @@ -1,8 +1,6 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { action, makeAutoObservable } from "mobx"; -import { Channel } from "revolt.js"; -import { Message } from "revolt.js"; -import { Nullable } from "revolt.js"; +import { Channel, Message, Nullable } from "revolt.js"; import { SimpleRenderer } from "./simple/SimpleRenderer"; import { RendererRoutines, ScrollState } from "./types"; diff --git a/src/lib/window.ts b/src/lib/window.ts new file mode 100644 index 00000000..7ee0ce3f --- /dev/null +++ b/src/lib/window.ts @@ -0,0 +1,20 @@ +/** + * Inject a key into the window's globals. + * @param key Key + * @param value Value + */ +export function injectWindow(key: string, value: any) { + (window as any)[key] = value; +} + +/** + * Inject a controller into the global controllers object. + * @param key Key + * @param value Value + */ +export function injectController(key: string, value: any) { + (window as any).controllers = { + ...((window as any).controllers ?? {}), + [key]: value, + }; +} diff --git a/src/main.tsx b/src/main.tsx index 28081391..2ba0dd7e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,21 +1,8 @@ -import { registerSW } from "virtual:pwa-register"; - import "./styles/index.scss"; import { render } from "preact"; -import { internalEmit } from "./lib/eventEmitter"; - import { App } from "./pages/app"; - -export const updateSW = registerSW({ - onNeedRefresh() { - internalEmit("PWA", "update"); - }, - onOfflineReady() { - console.info("Ready to work offline."); - // show a ready to work offline to user - }, -}); +import "./updateWorker"; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion render(, document.getElementById("app")!); diff --git a/src/mobx/State.ts b/src/mobx/State.ts index a65fc561..02a60e7e 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -1,22 +1,24 @@ // @ts-expect-error No typings. import stringify from "json-stringify-deterministic"; import localforage from "localforage"; -import { makeAutoObservable, reaction, runInAction } from "mobx"; -import { Client } from "revolt.js"; +import { action, makeAutoObservable, reaction, runInAction } from "mobx"; +import { Client, ClientboundNotification } from "revolt.js"; import { reportError } from "../lib/ErrorBoundary"; +import { injectWindow } from "../lib/window"; -import { legacyMigrateForwards, LegacyState } from "./legacy/redux"; - +import { clientController } from "../controllers/client/ClientController"; import Persistent from "./interfaces/Persistent"; import Syncable from "./interfaces/Syncable"; import Auth from "./stores/Auth"; +import Changelog from "./stores/Changelog"; import Draft from "./stores/Draft"; import Experiments from "./stores/Experiments"; import Layout from "./stores/Layout"; import LocaleOptions from "./stores/LocaleOptions"; import MessageQueue from "./stores/MessageQueue"; import NotificationOptions from "./stores/NotificationOptions"; +import Ordering from "./stores/Ordering"; import Plugins from "./stores/Plugins"; import ServerConfig from "./stores/ServerConfig"; import Settings from "./stores/Settings"; @@ -24,6 +26,7 @@ import Sync, { Data as DataSync, SyncKeys } from "./stores/Sync"; export const MIGRATIONS = { REDUX: 1640305719826, + MULTI_SERVER_CONFIG: 1656350006152, }; /** @@ -31,46 +34,51 @@ export const MIGRATIONS = { */ export default class State { auth: Auth; + changelog: Changelog; draft: Draft; locale: LocaleOptions; experiments: Experiments; layout: Layout; - config: ServerConfig; + /** + * DEPRECATED + */ + private config: ServerConfig; notifications: NotificationOptions; queue: MessageQueue; settings: Settings; sync: Sync; plugins: Plugins; + ordering: Ordering; private persistent: [string, Persistent][] = []; private disabled: Set = new Set(); - client?: Client; - /** * Construct new State. */ constructor() { this.auth = new Auth(); + this.changelog = new Changelog(); this.draft = new Draft(); this.locale = new LocaleOptions(); this.experiments = new Experiments(); this.layout = new Layout(); this.config = new ServerConfig(); - this.notifications = new NotificationOptions(); + this.notifications = new NotificationOptions(this); this.queue = new MessageQueue(); this.settings = new Settings(); this.sync = new Sync(this); this.plugins = new Plugins(this); + this.ordering = new Ordering(this); - makeAutoObservable(this, { - client: false, - }); + makeAutoObservable(this); this.register(); this.setDisabled = this.setDisabled.bind(this); + this.onPacket = this.onPacket.bind(this); - this.client = undefined; + // Inject into window + injectWindow("state", this); } /** @@ -127,6 +135,20 @@ export default class State { } } + /** + * Consume packets from the client. + * @param packet Inbound Packet + */ + @action onPacket(packet: ClientboundNotification) { + if (packet.type === "UserSettingsUpdate") { + try { + this.sync.apply(packet.update); + } catch (err) { + reportError(err as any, "failed_sync_apply"); + } + } + } + /** * Register reaction listeners for persistent data stores. * @returns Function to dispose of listeners @@ -134,8 +156,28 @@ export default class State { registerListeners(client?: Client) { // If a client is present currently, expose it and provide it to plugins. if (client) { - this.client = client; - this.plugins.onClient(client); + // Register message listener for clearing queue. + client.addListener("message", this.queue.onMessage); + + // Register listener for incoming packets. + client.addListener("packet", this.onPacket); + + // Register events for notifications. + client.addListener("message", this.notifications.onMessage); + client.addListener( + "user/relationship", + this.notifications.onRelationship, + ); + document.addEventListener( + "visibilitychange", + this.notifications.onVisibilityChange, + ); + + // Sync settings from remote server. + state.sync + .pull(client) + .catch(console.error) + .finally(() => state.changelog.checkForUpdates()); } // Register all the listeners required for saving and syncing state. @@ -221,8 +263,20 @@ export default class State { }); return () => { - // Stop exposing the client. - this.client = undefined; + // Remove any listeners attached to client. + if (client) { + client.removeListener("message", this.queue.onMessage); + client.removeListener("packet", this.onPacket); + client.removeListener("message", this.notifications.onMessage); + client.removeListener( + "user/relationship", + this.notifications.onRelationship, + ); + document.removeEventListener( + "visibilitychange", + this.notifications.onVisibilityChange, + ); + } // Wipe all listeners. listeners.forEach((x) => x()); @@ -233,23 +287,6 @@ export default class State { * Load data stores from local storage. */ async hydrate() { - // Migrate legacy Redux store. - try { - let legacy = await localforage.getItem("state"); - await localforage.removeItem("state"); - if (legacy) { - if (typeof legacy === "string") { - legacy = JSON.parse(legacy); - } - - legacyMigrateForwards(legacy as Partial, this); - await this.save(); - return; - } - } catch (err) { - reportError(err as any, "redux_migration"); - } - // Load MobX store. const sync = (await localforage.getItem("sync")) as DataSync; const { revision } = sync ?? { revision: {} }; @@ -266,6 +303,9 @@ export default class State { // Post-hydration, init plugins. this.plugins.init(); + + // Push authentication information forwards to client controller. + clientController.hydrate(this.auth); } /** @@ -276,10 +316,11 @@ export default class State { this.draft = new Draft(); this.experiments = new Experiments(); this.layout = new Layout(); - this.notifications = new NotificationOptions(); + this.notifications = new NotificationOptions(this); this.queue = new MessageQueue(); this.settings = new Settings(); this.sync = new Sync(this); + this.ordering = new Ordering(this); this.save(); @@ -289,13 +330,7 @@ export default class State { } } -var state: State; - -export async function hydrateState() { - state = new State(); - (window as any).state = state; - await state.hydrate(); -} +export const state = new State(); /** * Get the application state @@ -304,3 +339,11 @@ export async function hydrateState() { export function useApplicationState() { return state; } + +/** + * Get the application state + * @returns Application state + */ +export function getApplicationState() { + return state; +} diff --git a/src/mobx/legacy/redux.ts b/src/mobx/legacy/redux.ts deleted file mode 100644 index 589fa9c4..00000000 --- a/src/mobx/legacy/redux.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { runInAction } from "mobx"; -import { API } from "revolt.js"; - -import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme"; - -import { Language } from "../../../external/lang/Languages"; -import State from "../State"; -import { Data as DataAuth } from "../stores/Auth"; -import { Data as DataLocaleOptions } from "../stores/LocaleOptions"; -import { Data as DataNotificationOptions } from "../stores/NotificationOptions"; -import { ISettings } from "../stores/Settings"; -import { Data as DataSync } from "../stores/Sync"; - -export type LegacyTheme = Overrides & { - light?: boolean; - font?: Fonts; - css?: string; - monospaceFont?: MonospaceFonts; -}; - -export interface LegacyThemeOptions { - base?: string; - ligatures?: boolean; - custom?: Partial; -} - -export type LegacyEmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji"; -export interface LegacyAppearanceOptions { - emojiPack?: LegacyEmojiPacks; -} - -export type LegacyNotificationState = "all" | "mention" | "none" | "muted"; - -export type LegacyNotifications = { - [key: string]: LegacyNotificationState; -}; - -export interface LegacySyncData { - locale?: Language; - theme?: LegacyThemeOptions; - appearance?: LegacyAppearanceOptions; - notifications?: LegacyNotifications; -} - -export type LegacySyncKeys = - | "theme" - | "appearance" - | "locale" - | "notifications"; - -export interface LegacySyncOptions { - disabled?: LegacySyncKeys[]; - revision?: { - [key: string]: number; - }; -} - -export interface LegacyAuthState { - accounts: { - [key: string]: { - session: API.Session; - }; - }; - active?: string; -} - -export interface LegacySettings { - theme?: LegacyThemeOptions; - appearance?: LegacyAppearanceOptions; -} - -export function legacyMigrateAuth(auth: LegacyAuthState): DataAuth { - return { - current: auth.active, - sessions: auth.accounts, - }; -} - -export function legacyMigrateLocale(lang: Language): DataLocaleOptions { - return { - lang, - }; -} - -export function legacyMigrateTheme( - theme: LegacyThemeOptions, -): Partial { - const { light, font, css, monospaceFont, ...variables } = - theme.custom ?? {}; - - return { - "appearance:ligatures": theme.ligatures, - "appearance:theme:base": theme.base === "light" ? "light" : "dark", - "appearance:theme:light": light, - "appearance:theme:font": font, - "appearance:theme:monoFont": monospaceFont, - "appearance:theme:css": css, - "appearance:theme:overrides": variables, - }; -} - -export function legacyMigrateAppearance( - appearance: LegacyAppearanceOptions, -): Partial { - return { - "appearance:emoji": appearance.emojiPack, - }; -} - -/** - * Remove trolling from an object - * @param inp Object to remove trolling from - * @returns Object without trolling - */ -function detroll(inp: object): ISettings { - const obj: object = {}; - Object.keys(inp) - .filter((x) => typeof (inp as any)[x] !== "undefined") - .map((x) => ((obj as any)[x] = (inp as any)[x])); - - return obj as unknown as ISettings; -} - -export function legacyMigrateNotification( - channel: LegacyNotifications, -): DataNotificationOptions { - return { - channel, - }; -} - -export function legacyMigrateSync(sync: LegacySyncOptions): DataSync { - return { - disabled: sync.disabled ?? [], - revision: { - ...sync.revision, - }, - }; -} - -export type LegacyState = { - locale: Language; - auth: LegacyAuthState; - settings: LegacySettings; - sync: LegacySyncOptions; - notifications: LegacyNotifications; -}; - -export function legacyMigrateForwards( - data: Partial, - target: State, -) { - runInAction(() => { - if ("sync" in data) { - target.sync.hydrate(legacyMigrateSync(data.sync!)); - } - - if ("locale" in data) { - target.locale.hydrate(legacyMigrateLocale(data.locale!)); - } - - if ("auth" in data) { - target.auth.hydrate(legacyMigrateAuth(data.auth!)); - } - - if ("settings" in data) { - if (data!.settings!.theme) { - target.settings.hydrate( - detroll(legacyMigrateTheme(data.settings!.theme!)), - ); - } - - if (data!.settings!.appearance) { - target.settings.hydrate( - detroll( - legacyMigrateAppearance(data.settings!.appearance!), - ), - ); - } - } - - if ("notifications" in data) { - target.notifications.hydrate( - legacyMigrateNotification(data.notifications!), - ); - } - }); -} diff --git a/src/mobx/stores/Auth.ts b/src/mobx/stores/Auth.ts index bf1d0aec..12574350 100644 --- a/src/mobx/stores/Auth.ts +++ b/src/mobx/stores/Auth.ts @@ -1,19 +1,18 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; -import { API } from "revolt.js"; -import { Nullable } from "revolt.js"; import { mapToRecord } from "../../lib/conversion"; +import { clientController } from "../../controllers/client/ClientController"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; interface Account { - session: API.Session; + session: Session; + apiUrl?: string; } export interface Data { sessions: Record; - current?: string; } /** @@ -22,14 +21,12 @@ export interface Data { */ export default class Auth implements Store, Persistent { private sessions: ObservableMap; - private current: Nullable; /** * Construct new Auth store. */ constructor() { this.sessions = new ObservableMap(); - this.current = null; // Inject session token if it is provided. if (import.meta.env.VITE_SESSION_TOKEN) { @@ -40,8 +37,6 @@ export default class Auth implements Store, Persistent { token: import.meta.env.VITE_SESSION_TOKEN as string, }, }); - - this.current = "0"; } makeAutoObservable(this); @@ -54,7 +49,6 @@ export default class Auth implements Store, Persistent { @action toJSON() { return { sessions: JSON.parse(JSON.stringify(mapToRecord(this.sessions))), - current: this.current ?? undefined, }; } @@ -67,24 +61,20 @@ export default class Auth implements Store, Persistent { typeof data.sessions === "object" && data.sessions !== null ) { - let v = data.sessions; + const v = data.sessions; Object.keys(data.sessions).forEach((id) => this.sessions.set(id, v[id]), ); } - - if (data.current && this.sessions.has(data.current)) { - this.current = data.current; - } } /** * Add a new session to the auth manager. * @param session Session + * @param apiUrl Custom API URL */ - @action setSession(session: API.Session) { - this.sessions.set(session.user_id, { session }); - this.current = session.user_id; + @action setSession(session: Session, apiUrl?: string) { + this.sessions.set(session.user_id, { session, apiUrl }); } /** @@ -92,34 +82,39 @@ export default class Auth implements Store, Persistent { * @param user_id User ID tied to session */ @action removeSession(user_id: string) { - if (user_id == this.current) { - this.current = null; - } - this.sessions.delete(user_id); } + /** + * Get all known accounts. + * @returns Array of accounts + */ + @computed getAccounts() { + return [...this.sessions.values()]; + } + /** * Remove current session. */ - @action logout() { + /*@action logout() { this.current && this.removeSession(this.current); - } + }*/ /** * Get current session. * @returns Current session */ - @computed getSession() { + /*@computed getSession() { if (!this.current) return; return this.sessions.get(this.current)!.session; - } + }*/ /** * Check whether we are currently logged in. * @returns Whether we are logged in */ @computed isLoggedIn() { - return this.current !== null; + // ! FIXME: temp proxy info + return clientController.getActiveSession()?.ready; } } diff --git a/src/mobx/stores/Changelog.ts b/src/mobx/stores/Changelog.ts new file mode 100644 index 00000000..553117e0 --- /dev/null +++ b/src/mobx/stores/Changelog.ts @@ -0,0 +1,71 @@ +import { action, makeAutoObservable, runInAction } from "mobx"; + +import { latestChangelog } from "../../assets/changelogs"; +import { modalController } from "../../controllers/modals/ModalController"; +import Persistent from "../interfaces/Persistent"; +import Store from "../interfaces/Store"; +import Syncable from "../interfaces/Syncable"; + +export interface Data { + viewed?: number; +} + +/** + * Keeps track of viewed changelog items + */ +export default class Changelog implements Store, Persistent, Syncable { + /** + * Last viewed changelog ID + */ + private viewed: number; + + /** + * Construct new Layout store. + */ + constructor() { + this.viewed = 0; + makeAutoObservable(this); + } + + get id() { + return "changelog"; + } + + toJSON() { + return { + viewed: this.viewed, + }; + } + + @action hydrate(data: Data) { + if (data.viewed) { + this.viewed = data.viewed; + } + } + + apply(_key: string, data: unknown, _revision: number): void { + this.hydrate(data as Data); + } + + toSyncable(): { [key: string]: object } { + return { + changelog: this.toJSON(), + }; + } + + /** + * Check whether there are new updates + */ + checkForUpdates() { + if (this.viewed < latestChangelog) { + modalController.push({ + type: "changelog", + initial: latestChangelog, + }); + + runInAction(() => { + this.viewed = latestChangelog; + }); + } + } +} diff --git a/src/mobx/stores/Experiments.ts b/src/mobx/stores/Experiments.ts index 2d3f29f4..c37ad5d6 100644 --- a/src/mobx/stores/Experiments.ts +++ b/src/mobx/stores/Experiments.ts @@ -10,7 +10,7 @@ import Store from "../interfaces/Store"; /** * Union type of available experiments. */ -export type Experiment = "dummy" | "offline_users" | "plugins"; +export type Experiment = "dummy" | "offline_users" | "plugins" | "picker"; /** * Currently active experiments. @@ -19,6 +19,7 @@ export const AVAILABLE_EXPERIMENTS: Experiment[] = [ "dummy", "offline_users", "plugins", + "picker", ]; /** @@ -41,6 +42,11 @@ export const EXPERIMENTS: { description: "This will enable the experimental plugin API. Only touch this if you know what you're doing.", }, + picker: { + title: "Custom Emoji", + description: + "This will enable a work-in-progress emoji picker and custom emoji settings.", + }, }; export interface Data { diff --git a/src/mobx/stores/Layout.ts b/src/mobx/stores/Layout.ts index baaa157f..a6f1fd5e 100644 --- a/src/mobx/stores/Layout.ts +++ b/src/mobx/stores/Layout.ts @@ -58,6 +58,9 @@ export default class Layout implements Store, Persistent { this.lastDiscoverPath = "/discover/servers"; this.lastOpened = new ObservableMap(); this.openSections = new ObservableMap(); + + this.getLastHomePath = this.getLastHomePath.bind(this); + makeAutoObservable(this); } diff --git a/src/mobx/stores/LocaleOptions.ts b/src/mobx/stores/LocaleOptions.ts index 24a7161b..72abd6b2 100644 --- a/src/mobx/stores/LocaleOptions.ts +++ b/src/mobx/stores/LocaleOptions.ts @@ -31,6 +31,7 @@ export function findLanguage(lang?: string): Language { const value = Language[key as keyof typeof Language]; // Skip alternative/joke languages + if (Languages[value].cat === "const") continue; if (Languages[value].cat === "alt") continue; values.push(value); diff --git a/src/mobx/stores/MessageQueue.ts b/src/mobx/stores/MessageQueue.ts index 953427ed..c4fd5d26 100644 --- a/src/mobx/stores/MessageQueue.ts +++ b/src/mobx/stores/MessageQueue.ts @@ -5,6 +5,7 @@ import { makeAutoObservable, observable, } from "mobx"; +import { Message } from "revolt.js"; import Store from "../interfaces/Store"; @@ -47,6 +48,8 @@ export default class MessageQueue implements Store { constructor() { this.messages = observable.array([]); makeAutoObservable(this); + + this.onMessage = this.onMessage.bind(this); } get id() { @@ -105,4 +108,16 @@ export default class MessageQueue implements Store { @computed get(channel: string) { return this.messages.filter((x) => x.channel === channel); } + + /** + * Handle an incoming Message + * @param message Message + */ + @action onMessage(message: Message) { + if (!message.nonce) return; + if (!this.get(message.channel_id).find((x) => x.id === message.nonce)) + return; + + this.remove(message.nonce); + } } diff --git a/src/mobx/stores/NotificationOptions.ts b/src/mobx/stores/NotificationOptions.ts index 9632f048..91a5fe35 100644 --- a/src/mobx/stores/NotificationOptions.ts +++ b/src/mobx/stores/NotificationOptions.ts @@ -1,16 +1,14 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; -import { Channel } from "revolt.js"; -import { Message } from "revolt.js"; -import { Server } from "revolt.js"; +import { Channel, Message, Server, User } from "revolt.js"; +import { decodeTime } from "ulid"; + +import { translate } from "preact-i18n"; import { mapToRecord } from "../../lib/conversion"; -import { - legacyMigrateNotification, - LegacyNotifications, -} from "../legacy/redux"; +import { history, routeInformation } from "../../context/history"; -import { MIGRATIONS } from "../State"; +import State from "../State"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; import Syncable from "../interfaces/Syncable"; @@ -45,22 +43,54 @@ export interface Data { channel?: Record; } +/** + * Create a notification either directly or using service worker. + * @param title Notification Title + * @param options Notification Options + * @returns Notification + */ +async function createNotification( + title: string, + options: globalThis.NotificationOptions, +) { + try { + return new Notification(title, options); + } catch (err) { + const sw = await navigator.serviceWorker.getRegistration(); + sw?.showNotification(title, options); + } +} + /** * Manages the user's notification preferences. */ export default class NotificationOptions implements Store, Persistent, Syncable { + private state: State; + private activeNotifications: Record; + private server: ObservableMap; private channel: ObservableMap; /** * Construct new Experiments store. */ - constructor() { + constructor(state: State) { this.server = new ObservableMap(); this.channel = new ObservableMap(); - makeAutoObservable(this); + + makeAutoObservable(this, { + onMessage: false, + onRelationship: false, + }); + + this.state = state; + this.activeNotifications = {}; + + this.onMessage = this.onMessage.bind(this); + this.onRelationship = this.onRelationship.bind(this); + this.onVisibilityChange = this.onVisibilityChange.bind(this); } get id() { @@ -203,7 +233,7 @@ export default class NotificationOptions * @returns Whether this object is muted */ isMuted(target?: Channel | Server) { - var value: NotificationState | undefined; + let value: NotificationState | undefined; if (target instanceof Channel) { value = this.computeForChannel(target); } else if (target instanceof Server) { @@ -217,11 +247,246 @@ export default class NotificationOptions return false; } - @action apply(_key: "notifications", data: unknown, revision: number) { - if (revision < MIGRATIONS.REDUX) { - data = legacyMigrateNotification(data as LegacyNotifications); + /** + * Handle incoming messages and create a notification. + * @param message Message + */ + async onMessage(message: Message) { + // Ignore if we are currently looking and focused on the channel. + if ( + message.channel_id === routeInformation.getChannel() && + document.hasFocus() + ) + return; + + // Ignore if muted. + if (!this.shouldNotify(message)) return; + + // Play a sound and skip notif if disabled. + this.state.settings.sounds.playSound("message"); + if (!this.state.settings.get("notifications:desktop")) return; + + const effectiveName = + message.masquerade?.name ?? message.author?.username; + + let title; + switch (message.channel?.channel_type) { + case "SavedMessages": + return; + case "DirectMessage": + title = `@${effectiveName}`; + break; + case "Group": + if (message.author?._id === "00000000000000000000000000") { + title = message.channel.name; + } else { + title = `@${effectiveName} - ${message.channel.name}`; + } + break; + case "TextChannel": + title = `@${effectiveName} (#${message.channel.name}, ${message.channel.server?.name})`; + break; + default: + title = message.channel?._id; + break; } + let image; + if (message.attachments) { + const imageAttachment = message.attachments.find( + (x) => x.metadata.type === "Image", + ); + if (imageAttachment) { + image = message.client.generateFileURL(imageAttachment, { + max_side: 720, + }); + } + } + + let body, icon; + if (message.content) { + body = message.client.markdownToText(message.content); + + if (message.masquerade?.avatar) { + icon = message.client.proxyFile(message.masquerade.avatar); + } else { + icon = message.author?.generateAvatarURL({ max_side: 256 }); + } + } else if (message.system) { + const users = message.client.users; + + // ! FIXME: I've had to strip translations while + // ! I move stuff into the new project structure + switch (message.system.type) { + case "user_added": + case "user_remove": + { + const user = users.get(message.system.id); + body = `${user?.username} ${ + message.system.type === "user_added" + ? "added by" + : "removed by" + } ${users.get(message.system.by)?.username}`; + /*body = translate( + `app.main.channel.system.${ + message.system.type === "user_added" + ? "added_by" + : "removed_by" + }`, + { + user: user?.username, + other_user: users.get(message.system.by) + ?.username, + }, + );*/ + icon = user?.generateAvatarURL({ + max_side: 256, + }); + } + break; + case "user_joined": + case "user_left": + case "user_kicked": + case "user_banned": + { + const user = users.get(message.system.id); + body = `${user?.username}`; + /*body = translate( + `app.main.channel.system.${message.system.type}`, + { user: user?.username }, + );*/ + icon = user?.generateAvatarURL({ + max_side: 256, + }); + } + break; + case "channel_renamed": + { + const user = users.get(message.system.by); + body = `${user?.username} renamed channel to ${message.system.name}`; + /*body = translate( + `app.main.channel.system.channel_renamed`, + { + user: users.get(message.system.by)?.username, + name: message.system.name, + }, + );*/ + icon = user?.generateAvatarURL({ + max_side: 256, + }); + } + break; + case "channel_description_changed": + case "channel_icon_changed": + { + const user = users.get(message.system.by); + /*body = translate( + `app.main.channel.system.${message.system.type}`, + { user: users.get(message.system.by)?.username }, + );*/ + body = `${users.get(message.system.by)?.username}`; + icon = user?.generateAvatarURL({ + max_side: 256, + }); + } + break; + } + } + + const notif = await createNotification(title!, { + icon, + image, + body, + timestamp: decodeTime(message._id), + tag: message.channel?._id, + badge: "/assets/icons/android-chrome-512x512.png", + silent: true, + }); + + if (notif) { + notif.addEventListener("click", () => { + window.focus(); + + const id = message.channel_id; + if (id !== routeInformation.getChannel()) { + const channel = message.client.channels.get(id); + if (channel) { + if (channel.channel_type === "TextChannel") { + history.push( + `/server/${channel.server_id}/channel/${id}`, + ); + } else { + history.push(`/channel/${id}`); + } + } + } + }); + + this.activeNotifications[message.channel_id] = notif; + + notif.addEventListener( + "close", + () => delete this.activeNotifications[message.channel_id], + ); + } + } + + /** + * Handle user relationship changes. + * @param user User relationship changed with + */ + async onRelationship(user: User) { + // Ignore if disabled. + if (!this.state.settings.get("notifications:desktop")) return; + + // Check whether we are busy. + // This is checked by `shouldNotify` in the case of messages. + if (user.status?.presence === "Busy") { + return false; + } + + let event; + switch (user.relationship) { + case "Incoming": + /*event = translate("notifications.sent_request", { + person: user.username, + });*/ + event = `${user.username} sent you a friend request`; + break; + case "Friend": + /*event = translate("notifications.now_friends", { + person: user.username, + });*/ + event = `Now friends with ${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`); + }); + } + + /** + * Called when document visibility changes. + */ + onVisibilityChange() { + if (document.visibilityState === "visible") { + const channel_id = routeInformation.getChannel()!; + if (this.activeNotifications[channel_id]) { + this.activeNotifications[channel_id].close(); + } + } + } + + @action apply(_key: "notifications", data: unknown, _revision: number) { this.hydrate(data as Data); } diff --git a/src/mobx/stores/Ordering.ts b/src/mobx/stores/Ordering.ts new file mode 100644 index 00000000..a72e2ddb --- /dev/null +++ b/src/mobx/stores/Ordering.ts @@ -0,0 +1,95 @@ +import { action, computed, makeAutoObservable } from "mobx"; + +import { reorder } from "@revoltchat/ui"; + +import { clientController } from "../../controllers/client/ClientController"; +import State from "../State"; +import Persistent from "../interfaces/Persistent"; +import Store from "../interfaces/Store"; +import Syncable from "../interfaces/Syncable"; + +export interface Data { + servers?: string[]; +} + +/** + * Keeps track of ordering of various elements + */ +export default class Ordering implements Store, Persistent, Syncable { + private state: State; + + /** + * Ordered list of server IDs + */ + private servers: string[]; + + /** + * Construct new Layout store. + */ + constructor(state: State) { + this.servers = []; + makeAutoObservable(this); + + this.state = state; + this.reorderServer = this.reorderServer.bind(this); + } + + get id() { + return "ordering"; + } + + toJSON() { + return { + servers: this.servers, + }; + } + + @action hydrate(data: Data) { + if (data.servers) { + this.servers = data.servers; + } + } + + apply(_key: string, data: unknown, _revision: number): void { + this.hydrate(data as Data); + } + + toSyncable(): { [key: string]: object } { + return { + ordering: this.toJSON(), + }; + } + + /** + * All known servers with ordering applied + */ + @computed get orderedServers() { + const client = clientController.getReadyClient(); + const known = new Set(client?.servers.keys() ?? []); + const ordered = [...this.servers]; + + const out = []; + for (const id of ordered) { + if (known.delete(id)) { + out.push(client!.servers.get(id)!); + } + } + + for (const id of known) { + out.push(client!.servers.get(id)!); + } + + return out; + } + + /** + * Re-order a server + */ + @action reorderServer(source: number, dest: number) { + this.servers = reorder( + this.orderedServers.map((x) => x._id), + source, + dest, + ); + } +} diff --git a/src/mobx/stores/Plugins.ts b/src/mobx/stores/Plugins.ts index 506decb5..e6b3bbb3 100644 --- a/src/mobx/stores/Plugins.ts +++ b/src/mobx/stores/Plugins.ts @@ -41,7 +41,6 @@ type Plugin = { * ```typescript * function (state: State) { * return { - * onClient: (client: Client) => {}, * onUnload: () => {} * } * } @@ -59,7 +58,6 @@ type Plugin = { type Instance = { format: 1; - onClient?: (client: Client) => {}; onUnload?: () => void; }; @@ -124,7 +122,7 @@ export default class Plugins implements Store, Persistent { * @param id Plugin Id */ @computed get(namespace: string, id: string) { - return this.plugins.get(namespace + "/" + id); + return this.plugins.get(`${namespace}/${id}`); } /** @@ -133,7 +131,7 @@ export default class Plugins implements Store, Persistent { * @returns Plugin Instance */ private getInstance(plugin: Pick) { - return this.instances.get(plugin.namespace + "/" + plugin.id); + return this.instances.get(`${plugin.namespace}/${plugin.id}`); } /** @@ -154,12 +152,12 @@ export default class Plugins implements Store, Persistent { if (!this.state.experiments.isEnabled("plugins")) return console.error("Enable plugins in experiments!"); - let loaded = this.getInstance(plugin); + const loaded = this.getInstance(plugin); if (loaded) { this.unload(plugin.namespace, plugin.id); } - this.plugins.set(plugin.namespace + "/" + plugin.id, plugin); + this.plugins.set(`${plugin.namespace}/${plugin.id}`, plugin); if (typeof plugin.enabled === "undefined" || plugin) { this.load(plugin.namespace, plugin.id); @@ -173,7 +171,7 @@ export default class Plugins implements Store, Persistent { */ remove(namespace: string, id: string) { this.unload(namespace, id); - this.plugins.delete(namespace + "/" + id); + this.plugins.delete(`${namespace}/${id}`); } /** @@ -182,13 +180,13 @@ export default class Plugins implements Store, Persistent { * @param id Plugin Id */ load(namespace: string, id: string) { - let plugin = this.get(namespace, id); + const plugin = this.get(namespace, id); if (!plugin) throw "Unknown plugin!"; try { - let ns = plugin.namespace + "/" + plugin.id; + const ns = `${plugin.namespace}/${plugin.id}`; - let instance: Instance = eval(plugin.entrypoint)(); + const instance: Instance = eval(plugin.entrypoint)(); this.instances.set(ns, { ...instance, format: plugin.format, @@ -198,10 +196,6 @@ export default class Plugins implements Store, Persistent { ...plugin, enabled: true, }); - - if (this.state.client) { - instance.onClient?.(this.state.client); - } } catch (error) { console.error(`Failed to load ${namespace}/${id}!`); console.error(error); @@ -214,11 +208,11 @@ export default class Plugins implements Store, Persistent { * @param id Plugin Id */ unload(namespace: string, id: string) { - let plugin = this.get(namespace, id); + const plugin = this.get(namespace, id); if (!plugin) throw "Unknown plugin!"; - let ns = plugin.namespace + "/" + plugin.id; - let loaded = this.getInstance(plugin); + const ns = `${plugin.namespace}/${plugin.id}`; + const loaded = this.getInstance(plugin); if (loaded) { loaded.onUnload?.(); this.plugins.set(ns, { @@ -235,13 +229,4 @@ export default class Plugins implements Store, Persistent { localforage.removeItem("revite:plugins"); window.location.reload(); } - - /** - * Push client through to plugins - */ - onClient(client: Client) { - for (const instance of this.instances.values()) { - instance.onClient?.(client); - } - } } diff --git a/src/mobx/stores/ServerConfig.ts b/src/mobx/stores/ServerConfig.ts index 4212e82d..17ee1230 100644 --- a/src/mobx/stores/ServerConfig.ts +++ b/src/mobx/stores/ServerConfig.ts @@ -1,7 +1,5 @@ import { action, computed, makeAutoObservable } from "mobx"; -import { API } from "revolt.js"; -import { Client } from "revolt.js"; -import { Nullable } from "revolt.js"; +import { API, Client, Nullable } from "revolt.js"; import { isDebug } from "../../revision"; import Persistent from "../interfaces/Persistent"; diff --git a/src/mobx/stores/Settings.ts b/src/mobx/stores/Settings.ts index 8fed5083..344d65a7 100644 --- a/src/mobx/stores/Settings.ts +++ b/src/mobx/stores/Settings.ts @@ -2,19 +2,9 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; import { mapToRecord } from "../../lib/conversion"; -import { - LegacyAppearanceOptions, - legacyMigrateAppearance, - legacyMigrateTheme, - LegacyTheme, - LegacyThemeOptions, -} from "../legacy/redux"; - import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme"; -import { EmojiPack } from "../../components/common/Emoji"; - -import { MIGRATIONS } from "../State"; +import { EmojiPack, setGlobalEmojiPack } from "../../components/common/Emoji"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; import Syncable from "../interfaces/Syncable"; @@ -30,6 +20,7 @@ export interface ISettings { "appearance:ligatures": boolean; "appearance:seasonal": boolean; "appearance:transparency": boolean; + "appearance:show_send_button": boolean; "appearance:theme:base": "dark" | "light"; "appearance:theme:overrides": Partial; @@ -87,6 +78,11 @@ export default class Settings * @param value Value */ @action set(key: T, value: ISettings[T]) { + // Emoji needs to be immediately applied. + if (key === 'appearance:emoji') { + setGlobalEmojiPack(value as EmojiPack); + } + this.data.set(key, value); } @@ -128,16 +124,8 @@ export default class Settings @action apply( key: "appearance" | "theme", data: unknown, - revision: number, + _revision: number, ) { - if (revision < MIGRATIONS.REDUX) { - if (key === "appearance") { - data = legacyMigrateAppearance(data as LegacyAppearanceOptions); - } else { - data = legacyMigrateTheme(data as LegacyThemeOptions); - } - } - if (key === "appearance") { this.remove("appearance:emoji"); this.remove("appearance:seasonal"); @@ -158,7 +146,7 @@ export default class Settings @computed private pullKeys(keys: (keyof ISettings)[]) { const obj: Partial = {}; keys.forEach((key) => { - let value = this.get(key); + const value = this.get(key); if (!value) return; (obj as any)[key] = value; }); diff --git a/src/mobx/stores/Sync.ts b/src/mobx/stores/Sync.ts index 01acf8f4..52d1de9c 100644 --- a/src/mobx/stores/Sync.ts +++ b/src/mobx/stores/Sync.ts @@ -14,13 +14,21 @@ import State from "../State"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; -export type SyncKeys = "theme" | "appearance" | "locale" | "notifications"; +export type SyncKeys = + | "theme" + | "appearance" + | "locale" + | "notifications" + | "ordering" + | "changelog"; export const SYNC_KEYS: SyncKeys[] = [ "theme", "appearance", "locale", "notifications", + "ordering", + "changelog", ]; export interface Data { @@ -151,6 +159,13 @@ export default class Sync implements Store, Persistent { ); this.setRevision("notifications", notifications[0]); } + + const ordering = tryRead("ordering"); + if (ordering) { + this.state.setDisabled("ordering"); + this.state.ordering.apply("ordering", ordering[1], ordering[0]); + this.setRevision("ordering", ordering[0]); + } }); } diff --git a/src/mobx/stores/helpers/SAudio.ts b/src/mobx/stores/helpers/SAudio.ts index fdc4b504..a8256e3e 100644 --- a/src/mobx/stores/helpers/SAudio.ts +++ b/src/mobx/stores/helpers/SAudio.ts @@ -82,11 +82,11 @@ export default class SAudio { getAudio(path: string) { if (this.cache.has(path)) { return this.cache.get(path)!; - } else { + } const el = new Audio(path); this.cache.set(path, el); return el; - } + } loadCache() { @@ -100,7 +100,7 @@ export default class SAudio { try { audio.play(); } catch (err) { - console.error("Hit error while playing", sound + ":", err); + console.error("Hit error while playing", `${sound }:`, err); } } } diff --git a/src/mobx/stores/helpers/SSecurity.ts b/src/mobx/stores/helpers/SSecurity.ts index a57d8d1f..0fce7f93 100644 --- a/src/mobx/stores/helpers/SSecurity.ts +++ b/src/mobx/stores/helpers/SSecurity.ts @@ -2,6 +2,8 @@ import { makeAutoObservable, computed, action } from "mobx"; import Settings from "../Settings"; +const TRUSTED_DOMAINS = ["revolt.chat", "revolt.wtf", "gifbox.me", "rvlt.gg"]; + /** * Helper class for changing security options. */ @@ -27,6 +29,10 @@ export default class SSecurity { } @computed isTrustedOrigin(origin: string) { + if (TRUSTED_DOMAINS.find((x) => origin.endsWith(x))) { + return true; + } + return this.settings.get("security:trustedOrigins")?.includes(origin); } } diff --git a/src/mobx/stores/helpers/STheme.ts b/src/mobx/stores/helpers/STheme.ts index 9249d6ab..cde02cf3 100644 --- a/src/mobx/stores/helpers/STheme.ts +++ b/src/mobx/stores/helpers/STheme.ts @@ -110,7 +110,7 @@ export default class STheme { for (const key of Object.keys(variables)) { const value = variables[key]; if (typeof value === "string") { - variables[key + "-contrast"] = getContrastingColour(value); + variables[`${key }-contrast`] = getContrastingColour(value); } } diff --git a/src/pages/Open.tsx b/src/pages/Open.tsx index bd686e79..cf724c84 100644 --- a/src/pages/Open.tsx +++ b/src/pages/Open.tsx @@ -2,27 +2,22 @@ import { useHistory, useParams } from "react-router-dom"; import { Text } from "preact-i18n"; -import { useContext, useEffect } from "preact/hooks"; +import { useEffect } from "preact/hooks"; -import { useIntermediate } from "../context/intermediate/Intermediate"; -import { - AppContext, - ClientStatus, - StatusContext, -} from "../context/revoltjs/RevoltClient"; +import { Header } from "@revoltchat/ui"; -import Header from "../components/ui/Header"; +import { useSession } from "../controllers/client/ClientController"; +import { modalController } from "../controllers/modals/ModalController"; export default function Open() { const history = useHistory(); - const client = useContext(AppContext); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; const { id } = useParams<{ id: string }>(); - const { openScreen } = useIntermediate(); - if (status !== ClientStatus.ONLINE) { + if (session.state !== "Online") { return ( -
+
); @@ -40,7 +35,12 @@ export default function Open() { client .user!.openDM() .then((channel) => history.push(`/channel/${channel?._id}`)) - .catch((error) => openScreen({ id: "error", error })); + .catch((error) => + modalController.push({ + type: "error", + error, + }), + ); return; } @@ -62,7 +62,12 @@ export default function Open() { .get(id) ?.openDM() .then((channel) => history.push(`/channel/${channel?._id}`)) - .catch((error) => openScreen({ id: "error", error })); + .catch((error) => + modalController.push({ + type: "error", + error, + }), + ); } return; @@ -72,7 +77,7 @@ export default function Open() { }); return ( -
+
); diff --git a/src/pages/RevoltApp.tsx b/src/pages/RevoltApp.tsx index 93b623a1..23849dd6 100644 --- a/src/pages/RevoltApp.tsx +++ b/src/pages/RevoltApp.tsx @@ -2,22 +2,16 @@ import { Docked, OverlappingPanels, ShowIf } from "react-overlapping-panels"; import { Switch, Route, useLocation, Link } from "react-router-dom"; import styled, { css } from "styled-components/macro"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import ContextMenus from "../lib/ContextMenus"; import { isTouchscreenDevice } from "../lib/isTouchscreenDevice"; -import { useApplicationState } from "../mobx/State"; -import { SIDEBAR_CHANNELS } from "../mobx/stores/Layout"; - -import Popovers from "../context/intermediate/Popovers"; -import Notifications from "../context/revoltjs/Notifications"; -import StateMonitor from "../context/revoltjs/StateMonitor"; - import { Titlebar } from "../components/native/Titlebar"; import BottomNavigation from "../components/navigation/BottomNavigation"; import LeftSidebar from "../components/navigation/LeftSidebar"; import RightSidebar from "../components/navigation/RightSidebar"; +import { useSystemAlert } from "../updateWorker"; import Open from "./Open"; import Channel from "./channels/Channel"; import Developer from "./developer/Developer"; @@ -79,12 +73,6 @@ const Routes = styled.div.attrs({ "data-component": "routes" })<{ background: var(--primary-background); - /*background-color: rgba( - var(--primary-background-rgb), - max(var(--min-opacity), 0.75) - );*/ - //backdrop-filter: blur(10px); - ${() => isTouchscreenDevice && css` @@ -112,24 +100,35 @@ export default function App() { path.startsWith("/invite") || path.includes("/settings"); - const [statusBar, setStatusBar] = useState(true); + const alert = useSystemAlert(); + const [statusBar, setStatusBar] = useState(false); + useEffect(() => setStatusBar(true), [alert]); return ( <> - {statusBar && ( + {alert && statusBar && ( -
- Planned Maintenance at 18:00 UTC (7th May 2022) -
-
- -
Updates
-
- setStatusBar(false)}> -
Dismiss
-
+
{alert.text}
+
+ {alert.actions?.map((action) => + action.type === "internal" ? ( + +
{action.text}
+ + ) : action.type === "external" ? ( + +
{action.text}
{" "} +
+ ) : null, + )} + {alert.dismissable !== false && ( + setStatusBar(false)}> +
Dismiss
+
+ )}
)} @@ -140,11 +139,11 @@ export default function App() { - - - diff --git a/src/pages/app.tsx b/src/pages/app.tsx index 4fb2093a..f84078df 100644 --- a/src/pages/app.tsx +++ b/src/pages/app.tsx @@ -2,57 +2,66 @@ import { Route, Switch } from "react-router-dom"; import { lazy, Suspense } from "preact/compat"; +import { Masks, Preloader } from "@revoltchat/ui"; + import ErrorBoundary from "../lib/ErrorBoundary"; -import FakeClient from "../lib/FakeClient"; import Context from "../context"; -import { CheckAuth } from "../context/revoltjs/CheckAuth"; - -import Masks from "../components/ui/Masks"; -import Preloader from "../components/ui/Preloader"; +import { CheckAuth } from "../controllers/client/jsx/CheckAuth"; import Invite from "./invite/Invite"; const Login = lazy(() => import("./login/Login")); +const ConfirmDelete = lazy(() => import("./login/ConfirmDelete")); const RevoltApp = lazy(() => import("./RevoltApp")); +const LoadSuspense: React.FC = ({ children }) => ( + // @ts-expect-error Typing issue between Preact and Preact. + }>{children} +); + export function App() { return ( - {/* - // @ts-expect-error typings mis-match between preact... and preact? */} - }> - - + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + + + - - - - + + + + ); diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx index f27f5317..3545fd55 100644 --- a/src/pages/channels/Channel.tsx +++ b/src/pages/channels/Channel.tsx @@ -16,16 +16,14 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout"; -import { useClient } from "../../context/revoltjs/RevoltClient"; - import AgeGate from "../../components/common/AgeGate"; import MessageBox from "../../components/common/messaging/MessageBox"; import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom"; import NewMessages from "../../components/common/messaging/bars/NewMessages"; import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator"; -import { PageHeader } from "../../components/ui/Header"; - import RightSidebar from "../../components/navigation/RightSidebar"; +import { PageHeader } from "../../components/ui/Header"; +import { useClient } from "../../controllers/client/ClientController"; import ChannelHeader from "./ChannelHeader"; import { MessageArea } from "./messaging/MessageArea"; import VoiceHeader from "./voice/VoiceHeader"; @@ -100,14 +98,23 @@ const PlaceholderBase = styled.div` export const Channel = observer( ({ id, server_id }: { id: string; server_id: string }) => { const client = useClient(); + const state = useApplicationState(); if (!client.channels.exists(id)) { if (server_id) { const server = client.servers.get(server_id); if (server && server.channel_ids.length > 0) { + let target_id = server.channel_ids[0]; + const last_id = state.layout.getLastOpened(server_id); + if (last_id) { + if (client.channels.has(last_id)) { + target_id = last_id; + } + } + return ( ); } diff --git a/src/pages/channels/ChannelHeader.tsx b/src/pages/channels/ChannelHeader.tsx index 69f32d50..e4c20c80 100644 --- a/src/pages/channels/ChannelHeader.tsx +++ b/src/pages/channels/ChannelHeader.tsx @@ -1,31 +1,17 @@ -import { - At, - ChevronLeft, - ChevronRight, - Hash, -} from "@styled-icons/boxicons-regular"; +import { At, Hash } from "@styled-icons/boxicons-regular"; import { Notepad, Group } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; -import { Channel } from "revolt.js"; -import { User } from "revolt.js"; -import styled, { css } from "styled-components/macro"; +import { Channel, User } from "revolt.js"; +import styled from "styled-components/macro"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; -import { useApplicationState } from "../../mobx/State"; -import { SIDEBAR_CHANNELS, SIDEBAR_MEMBERS } from "../../mobx/stores/Layout"; - -import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { getChannelName } from "../../context/revoltjs/util"; - import { useStatusColour } from "../../components/common/user/UserIcon"; import UserStatus from "../../components/common/user/UserStatus"; -import Header, { - HamburgerAction, - PageHeader, -} from "../../components/ui/Header"; - import Markdown from "../../components/markdown/Markdown"; +import { PageHeader } from "../../components/ui/Header"; +import { ChannelName } from "../../controllers/client/jsx/ChannelName"; +import { modalController } from "../../controllers/modals/ModalController"; import HeaderActions from "./actions/HeaderActions"; export interface ChannelHeaderProps { @@ -77,9 +63,6 @@ const Info = styled.div` `; export default observer(({ channel }: ChannelHeaderProps) => { - const { openScreen } = useIntermediate(); - - const name = getChannelName(channel); let icon, recipient: User | undefined; switch (channel.channel_type) { case "SavedMessages": @@ -98,9 +81,11 @@ export default observer(({ channel }: ChannelHeaderProps) => { } return ( - + - {name} + + + {isTouchscreenDevice && channel.channel_type === "DirectMessage" && ( <> @@ -126,8 +111,8 @@ export default observer(({ channel }: ChannelHeaderProps) => { - openScreen({ - id: "channel_info", + modalController.push({ + type: "channel_info", channel, }) }> diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx index 8323b355..cf13311b 100644 --- a/src/pages/channels/actions/HeaderActions.tsx +++ b/src/pages/channels/actions/HeaderActions.tsx @@ -11,6 +11,8 @@ import { observer } from "mobx-react-lite"; import { useHistory } from "react-router-dom"; import styled, { css } from "styled-components/macro"; +import { IconButton } from "@revoltchat/ui"; + import { chainedDefer, defer } from "../../../lib/defer"; import { internalEmit } from "../../../lib/eventEmitter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; @@ -19,11 +21,8 @@ import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; import { useApplicationState } from "../../../mobx/State"; import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - import UpdateIndicator from "../../../components/common/UpdateIndicator"; -import IconButton from "../../../components/ui/IconButton"; - +import { modalController } from "../../../controllers/modals/ModalController"; import { ChannelHeaderProps } from "../ChannelHeader"; const Container = styled.div` @@ -73,7 +72,6 @@ const SearchBar = styled.div` export default function HeaderActions({ channel }: ChannelHeaderProps) { const layout = useApplicationState().layout; - const { openScreen } = useIntermediate(); const history = useHistory(); function slideOpen() { @@ -114,8 +112,8 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) { <> - openScreen({ - id: "user_picker", + modalController.push({ + type: "user_picker", omit: channel.recipient_ids!, callback: async (users) => { for (const user of users) { diff --git a/src/pages/channels/messaging/ConversationStart.tsx b/src/pages/channels/messaging/ConversationStart.tsx index 09ebf235..ced692c4 100644 --- a/src/pages/channels/messaging/ConversationStart.tsx +++ b/src/pages/channels/messaging/ConversationStart.tsx @@ -4,7 +4,7 @@ import styled from "styled-components/macro"; import { Text } from "preact-i18n"; -import { getChannelName } from "../../../context/revoltjs/util"; +import { ChannelName } from "../../../controllers/client/jsx/ChannelName"; const StartBase = styled.div` margin: 18px 16px 10px 16px; @@ -28,7 +28,9 @@ interface Props { export default observer(({ channel }: Props) => { return ( -

{getChannelName(channel, true)}

+

+ +

diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index e954eac3..d0ae8a5f 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -16,20 +16,16 @@ import { useState, } from "preact/hooks"; +import { Preloader } from "@revoltchat/ui"; + import { defer } from "../../../lib/defer"; import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter"; import { getRenderer } from "../../../lib/renderer/Singleton"; import { ScrollState } from "../../../lib/renderer/types"; -import { IntermediateContext } from "../../../context/intermediate/Intermediate"; -import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; -import { - ClientStatus, - StatusContext, -} from "../../../context/revoltjs/RevoltClient"; - -import Preloader from "../../../components/ui/Preloader"; - +import { useSession } from "../../../controllers/client/ClientController"; +import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline"; +import { modalController } from "../../../controllers/modals/ModalController"; import ConversationStart from "./ConversationStart"; import MessageRenderer from "./MessageRenderer"; @@ -65,8 +61,7 @@ export const MESSAGE_AREA_PADDING = 82; export const MessageArea = observer(({ last_id, channel }: Props) => { const history = useHistory(); - const status = useContext(StatusContext); - const { focusTaken } = useContext(IntermediateContext); + const session = useSession()!; // ? Required data for message links. const { message } = useParams<{ message: string }>(); @@ -213,8 +208,8 @@ export const MessageArea = observer(({ last_id, channel }: Props) => { // ? If we are waiting for network, try again. useEffect(() => { - switch (status) { - case ClientStatus.ONLINE: + switch (session.state) { + case "Online": if (renderer.state === "WAITING_FOR_NETWORK") { renderer.init(); } else { @@ -222,13 +217,13 @@ export const MessageArea = observer(({ last_id, channel }: Props) => { } break; - case ClientStatus.OFFLINE: - case ClientStatus.DISCONNECTED: - case ClientStatus.CONNECTING: + case "Offline": + case "Disconnected": + case "Connecting": renderer.markStale(); break; } - }, [renderer, status]); + }, [renderer, session.state]); // ? When the container is scrolled. // ? Also handle StayAtBottom @@ -306,7 +301,7 @@ export const MessageArea = observer(({ last_id, channel }: Props) => { // ? Scroll to bottom when pressing 'Escape'. useEffect(() => { function keyUp(e: KeyboardEvent) { - if (e.key === "Escape" && !focusTaken) { + if (e.key === "Escape" && !modalController.isVisible) { renderer.jumpToBottom(true); internalEmit("TextArea", "focus", "message"); } @@ -314,7 +309,7 @@ export const MessageArea = observer(({ last_id, channel }: Props) => { document.body.addEventListener("keyup", keyUp); return () => document.body.removeEventListener("keyup", keyUp); - }, [renderer, ref, focusTaken]); + }, [renderer, ref]); return ( { function keyUp(e: KeyboardEvent) { - if (e.key === "Escape" && !focusTaken) { + if (e.key === "Escape" && !modalController.isVisible) { finish(); } } document.body.addEventListener("keyup", keyUp); return () => document.body.removeEventListener("keyup", keyUp); - }, [focusTaken, finish]); + }, [finish]); const { onChange, diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index 939cee23..950c56d5 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -1,30 +1,26 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { X } from "@styled-icons/boxicons-regular"; +import dayjs from "dayjs"; import isEqual from "lodash.isequal"; import { observer } from "mobx-react-lite"; -import { API } from "revolt.js"; -import { Message as MessageI } from "revolt.js"; -import { Nullable } from "revolt.js"; +import { API, Message as MessageI, Nullable } from "revolt.js"; import styled from "styled-components/macro"; import { decodeTime } from "ulid"; import { Text } from "preact-i18n"; import { useEffect, useState } from "preact/hooks"; +import { MessageDivider, Preloader } from "@revoltchat/ui"; + import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter"; import { ChannelRenderer } from "../../../lib/renderer/Singleton"; import { useApplicationState } from "../../../mobx/State"; -import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - import Message from "../../../components/common/messaging/Message"; import { SystemMessage } from "../../../components/common/messaging/SystemMessage"; -import DateDivider from "../../../components/ui/DateDivider"; -import Preloader from "../../../components/ui/Preloader"; - -import { Children } from "../../../types/Preact"; +import { useClient } from "../../../controllers/client/ClientController"; +import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline"; import ConversationStart from "./ConversationStart"; import MessageEditor from "./MessageEditor"; @@ -125,7 +121,12 @@ export default observer(({ last_id, renderer, highlight }: Props) => { } if (unread || date) { - render.push(); + render.push( + , + ); head = true; } diff --git a/src/pages/channels/voice/VoiceHeader.tsx b/src/pages/channels/voice/VoiceHeader.tsx index 92634247..618baaa2 100644 --- a/src/pages/channels/voice/VoiceHeader.tsx +++ b/src/pages/channels/voice/VoiceHeader.tsx @@ -6,22 +6,20 @@ import { VolumeFull, VolumeMute, } from "@styled-icons/boxicons-solid"; -import { Hashnode, Speakerdeck, Teamspeak } from "@styled-icons/simple-icons"; import { observer } from "mobx-react-lite"; import styled from "styled-components/macro"; import { Text } from "preact-i18n"; import { useMemo } from "preact/hooks"; -import VoiceClient from "../../../lib/vortex/VoiceClient"; -import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; +import { Button } from "@revoltchat/ui"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; +import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; import Tooltip from "../../../components/common/Tooltip"; import UserIcon from "../../../components/common/user/UserIcon"; -import Button from "../../../components/ui/Button"; +import { useClient } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; interface Props { id: string; @@ -85,8 +83,6 @@ const VoiceBase = styled.div` export default observer(({ id }: Props) => { if (voiceState.roomId !== id) return null; - const { openScreen } = useIntermediate(); - const client = useClient(); const self = client.users.get(client.user!._id); @@ -101,26 +97,27 @@ export default observer(({ id }: Props) => {
{users && users.length !== 0 ? users.map((user, index) => { - const id = keys![index]; + const user_id = keys![index]; return ( -
+
- openScreen({ - id: "profile", - user_id: id, + modalController.push({ + type: "user_profile", + user_id, }) } /> @@ -145,7 +142,7 @@ export default observer(({ id }: Props) => {
- diff --git a/src/pages/developer/Developer.tsx b/src/pages/developer/Developer.tsx index d64cf5f6..c9e7f617 100644 --- a/src/pages/developer/Developer.tsx +++ b/src/pages/developer/Developer.tsx @@ -1,18 +1,17 @@ import { Wrench } from "@styled-icons/boxicons-solid"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import PaintCounter from "../../lib/PaintCounter"; import { TextReact } from "../../lib/i18n"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; - -import Header, { PageHeader } from "../../components/ui/Header"; +import { PageHeader } from "../../components/ui/Header"; +import { useClient } from "../../controllers/client/ClientController"; export default function Developer() { // const voice = useContext(VoiceContext); - const client = useContext(AppContext); + const client = useClient(); const userPermission = client.user!.permission; const [ping, setPing] = useState(client.websocket.ping); const [crash, setCrash] = useState(false); diff --git a/src/pages/discover/Discover.tsx b/src/pages/discover/Discover.tsx index d596ea72..666daeed 100644 --- a/src/pages/discover/Discover.tsx +++ b/src/pages/discover/Discover.tsx @@ -5,15 +5,15 @@ import styled, { css } from "styled-components/macro"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { Header, Preloader } from "@revoltchat/ui"; + import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; import { Overrides } from "../../context/Theme"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; -import Header from "../../components/ui/Header"; -import Preloader from "../../components/ui/Preloader"; +import { modalController } from "../../controllers/modals/ModalController"; const Container = styled.div` flex-grow: 1; @@ -86,7 +86,6 @@ const REMOTE = "https://rvlt.gg"; export default function Discover() { const state = useApplicationState(); - const { openLink } = useIntermediate(); const history = useHistory(); const { pathname, search } = useLocation(); @@ -121,11 +120,11 @@ export default function Discover() { useEffect(() => { function onMessage(message: MessageEvent) { - let url = new URL(message.origin); + const url = new URL(message.origin); if (!TRUSTED_HOSTS.includes(url.host)) return; try { - let data = JSON.parse(message.data); + const data = JSON.parse(message.data); if (data.source === "discover") { switch (data.type) { case "init": { @@ -138,7 +137,7 @@ export default function Discover() { break; } case "navigate": { - openLink(data.url); + modalController.openLink(data.url); break; } case "applyTheme": { @@ -164,7 +163,7 @@ export default function Discover() { return ( {isTouchscreenDevice && ( -
+
Discover
diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx index 6393ace5..4bdd9c7c 100644 --- a/src/pages/friends/Friend.tsx +++ b/src/pages/friends/Friend.tsx @@ -6,20 +6,17 @@ import { User } from "revolt.js"; import styles from "./Friend.module.scss"; import classNames from "classnames"; -import { Ref } from "preact"; import { useTriggerEvents } from "preact-context-menu"; import { Text } from "preact-i18n"; +import { IconButton } from "@revoltchat/ui"; + import { stopPropagation } from "../../lib/stopPropagation"; import { voiceState } from "../../lib/vortex/VoiceState"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; - import UserIcon from "../../components/common/user/UserIcon"; import UserStatus from "../../components/common/user/UserStatus"; -import IconButton from "../../components/ui/IconButton"; - -import { Children } from "../../types/Preact"; +import { modalController } from "../../controllers/modals/ModalController"; interface Props { user: User; @@ -27,7 +24,6 @@ interface Props { export const Friend = observer(({ user }: Props) => { const history = useHistory(); - const { openScreen } = useIntermediate(); const actions: Children[] = []; let subtext: Children = null; @@ -37,7 +33,7 @@ export const Friend = observer(({ user }: Props) => { actions.push( <> stopPropagation( @@ -51,7 +47,7 @@ export const Friend = observer(({ user }: Props) => { stopPropagation( @@ -72,7 +68,7 @@ export const Friend = observer(({ user }: Props) => { if (user.relationship === "Incoming") { actions.push( stopPropagation(ev, user.addFriend())}> @@ -93,7 +89,7 @@ export const Friend = observer(({ user }: Props) => { ) { actions.push( { stopPropagation( ev, user.relationship === "Friend" - ? openScreen({ - id: "special_prompt", + ? modalController.push({ type: "unfriend_user", target: user, }) @@ -119,7 +114,7 @@ export const Friend = observer(({ user }: Props) => { if (user.relationship === "Blocked") { actions.push( stopPropagation(ev, user.unblockUser())}> @@ -130,7 +125,12 @@ export const Friend = observer(({ user }: Props) => { return (
openScreen({ id: "profile", user_id: user._id })} + onClick={() => + modalController.push({ + type: "user_profile", + user_id: user._id, + }) + } {...useTriggerEvents("Menu", { user: user._id, })}> diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx index 1eefebb9..e4374a26 100644 --- a/src/pages/friends/Friends.tsx +++ b/src/pages/friends/Friends.tsx @@ -7,24 +7,20 @@ import styles from "./Friend.module.scss"; import classNames from "classnames"; import { Text } from "preact-i18n"; +import { IconButton } from "@revoltchat/ui"; + import { TextReact } from "../../lib/i18n"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { useClient } from "../../context/revoltjs/RevoltClient"; - import CollapsibleSection from "../../components/common/CollapsibleSection"; import Tooltip from "../../components/common/Tooltip"; import UserIcon from "../../components/common/user/UserIcon"; import { PageHeader } from "../../components/ui/Header"; -import IconButton from "../../components/ui/IconButton"; - -import { Children } from "../../types/Preact"; +import { useClient } from "../../controllers/client/ClientController"; +import { modalController } from "../../controllers/modals/ModalController"; import { Friend } from "./Friend"; export default observer(() => { - const { openScreen } = useIntermediate(); - const client = useClient(); const users = [...client.users.values()]; users.sort((a, b) => a.username.localeCompare(b.username)); @@ -68,7 +64,10 @@ export default observer(() => { const isEmpty = lists.reduce((p: number, n) => p + n.length, 0) === 0; return ( <> - } transparent noBurger> + } + withTransparency + noBurger>
@@ -82,8 +81,7 @@ export default observer(() => { - openScreen({ - id: "special_input", + modalController.push({ type: "create_group", }) }> @@ -93,8 +91,7 @@ export default observer(() => { - openScreen({ - id: "special_input", + modalController.push({ type: "add_friend", }) }> @@ -127,8 +124,8 @@ export default observer(() => {
- openScreen({ - id: "pending_requests", + modalController.push({ + type: "pending_friend_requests", users: incoming, }) }> @@ -198,7 +195,7 @@ export default observer(() => { sticky large summary={ -
+
— {list.length}
}> diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index ef66bca9..75609150 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -15,18 +15,19 @@ import styled from "styled-components/macro"; import styles from "./Home.module.scss"; import "./snow.scss"; import { Text } from "preact-i18n"; -import { useContext, useMemo } from "preact/hooks"; +import { useMemo } from "preact/hooks"; + +import { CategoryButton } from "@revoltchat/ui"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; +import wideSVG from "/assets/wide.svg"; import { PageHeader } from "../../components/ui/Header"; -import CategoryButton from "../../components/ui/fluent/CategoryButton"; -import wideSVG from "/assets/wide.svg"; +import { useClient } from "../../controllers/client/ClientController"; +import { modalController } from "../../controllers/modals/ModalController"; const Overlay = styled.div` display: grid; @@ -42,8 +43,7 @@ const Overlay = styled.div` `; export default observer(() => { - const { openScreen } = useIntermediate(); - const client = useContext(AppContext); + const client = useClient(); const state = useApplicationState(); const seasonalTheme = state.settings.get("appearance:seasonal", true); @@ -75,16 +75,16 @@ export default observer(() => {
{seasonalTheme && ( -
+
{snowflakes.map((emoji, index) => ( -
+
{emoji}
))}
)}
- } transparent> + } withTransparency>
@@ -96,8 +96,7 @@ export default observer(() => {
- openScreen({ - id: "special_input", + modalController.push({ type: "create_group", }) }> diff --git a/src/pages/invite/Invite.module.scss b/src/pages/invite/Invite.module.scss index 36b50ddd..7fb775c9 100644 --- a/src/pages/invite/Invite.module.scss +++ b/src/pages/invite/Invite.module.scss @@ -38,7 +38,7 @@ padding: 32px 16px 16px 16px; background: rgba(0, 0, 0, 0.6); border-radius: var(--border-radius); - + h1 { margin: 0; font-weight: 500; @@ -63,7 +63,6 @@ button { margin: auto; display: block; - background: rgba(0, 0, 0, 0.8); } } } diff --git a/src/pages/invite/Invite.tsx b/src/pages/invite/Invite.tsx index 801426ab..b7e3e16e 100644 --- a/src/pages/invite/Invite.tsx +++ b/src/pages/invite/Invite.tsx @@ -1,38 +1,33 @@ import { ArrowBack } from "@styled-icons/boxicons-regular"; -import { autorun } from "mobx"; import { Redirect, useHistory, useParams } from "react-router-dom"; import { API } from "revolt.js"; import styles from "./Invite.module.scss"; import { Text } from "preact-i18n"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; + +import { Button, Category, Error, Preloader } from "@revoltchat/ui"; -import { defer } from "../../lib/defer"; import { TextReact } from "../../lib/i18n"; import { useApplicationState } from "../../mobx/State"; -import RequiresOnline from "../../context/revoltjs/RequiresOnline"; -import { - AppContext, - ClientStatus, - StatusContext, -} from "../../context/revoltjs/RevoltClient"; -import { takeError } from "../../context/revoltjs/util"; - import ServerIcon from "../../components/common/ServerIcon"; import UserIcon from "../../components/common/user/UserIcon"; -import Button from "../../components/ui/Button"; -import Overline from "../../components/ui/Overline"; -import Preloader from "../../components/ui/Preloader"; +import { + useClient, + useSession, +} from "../../controllers/client/ClientController"; +import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; +import { takeError } from "../../controllers/client/jsx/error"; export default function Invite() { const history = useHistory(); - const client = useContext(AppContext); + const session = useSession(); + const client = useClient(); const layout = useApplicationState().layout; - const status = useContext(StatusContext); const { code } = useParams<{ code: string }>(); const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); @@ -47,7 +42,7 @@ export default function Invite() { .then((data) => setInvite(data)) .catch((err) => setError(takeError(err))); } - }, [client, code, invite, status]); + }, [code, invite]); if (code === undefined) return ; @@ -71,7 +66,7 @@ export default function Invite() {
- - Add to group + Add to group {page === "create" && ( - <> - - {" "} - - - - - - {" "} - - - - - + + {" "} + + + + )} {page === "login" && ( <> @@ -238,6 +241,31 @@ export const Form = observer(({ page, callback }: Props) => { + + {" "} + + + + + {import.meta.env.VITE_API_URL && + import.meta.env.VITE_API_URL != + "https://api.revolt.chat" && ( + <> +
+ + + {" "} + + + + + + + )} )} {(page === "reset" || diff --git a/src/pages/login/forms/FormCreate.tsx b/src/pages/login/forms/FormCreate.tsx index 8d2e9ed7..65b4305f 100644 --- a/src/pages/login/forms/FormCreate.tsx +++ b/src/pages/login/forms/FormCreate.tsx @@ -1,9 +1,7 @@ -import { useApplicationState } from "../../../mobx/State"; - +import { useClient } from "../../../controllers/client/ClientController"; import { Form } from "./Form"; export function FormCreate() { - const config = useApplicationState().config; - const client = config.createClient(); + const client = useClient(); return
client.register(data)} />; } diff --git a/src/pages/login/forms/FormLogin.tsx b/src/pages/login/forms/FormLogin.tsx index e827b197..d32db521 100644 --- a/src/pages/login/forms/FormLogin.tsx +++ b/src/pages/login/forms/FormLogin.tsx @@ -1,81 +1,6 @@ -import { detect } from "detect-browser"; -import { API } from "revolt.js"; - -import { useApplicationState } from "../../../mobx/State"; - -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - +import { clientController } from "../../../controllers/client/ClientController"; import { Form } from "./Form"; export function FormLogin() { - const state = useApplicationState(); - const { openScreen } = useIntermediate(); - - return ( - { - const browser = detect(); - let friendly_name; - if (browser) { - let { name } = browser; - const { os } = browser; - let isiPad; - if (window.isNative) { - friendly_name = `Revolt Desktop on ${os}`; - } else { - if (name === "ios") { - name = "safari"; - } else if (name === "fxios") { - name = "firefox"; - } else if (name === "crios") { - name = "chrome"; - } - if (os === "Mac OS" && navigator.maxTouchPoints > 0) - isiPad = true; - friendly_name = `${name} on ${isiPad ? "iPadOS" : os}`; - } - } else { - friendly_name = "Unknown Device"; - } - - // ! FIXME: temporary login flow code - // This should be replaced in the future. - const client = state.config.createClient(); - await client.fetchConfiguration(); - const session = await client.api.post("/auth/session/login", { - ...data, - friendly_name, - }); - - if (session.result !== "Success") { - alert("unsupported!"); - return; - } - - const s = session; - - client.session = session; - (client as any).$updateHeaders(); - - async function login() { - state.auth.setSession(s); - } - - const { onboarding } = await client.api.get("/onboard/hello"); - - if (onboarding) { - openScreen({ - id: "onboarding", - callback: async (username: string) => - client - .completeOnboarding({ username }, false) - .then(login), - }); - } else { - login(); - } - }} - /> - ); + return ; } diff --git a/src/pages/login/forms/FormReset.tsx b/src/pages/login/forms/FormReset.tsx index f71fec44..a3691a63 100644 --- a/src/pages/login/forms/FormReset.tsx +++ b/src/pages/login/forms/FormReset.tsx @@ -1,22 +1,16 @@ import { useHistory, useParams } from "react-router-dom"; -import { useContext } from "preact/hooks"; - -import { useApplicationState } from "../../../mobx/State"; - -import { AppContext } from "../../../context/revoltjs/RevoltClient"; - +import { useApi } from "../../../controllers/client/ClientController"; import { Form } from "./Form"; export function FormSendReset() { - const config = useApplicationState().config; - const client = config.createClient(); + const api = useApi(); return ( { - await client.api.post("/auth/account/reset_password", data); + await api.post("/auth/account/reset_password", data); }} /> ); @@ -24,15 +18,14 @@ export function FormSendReset() { export function FormReset() { const { token } = useParams<{ token: string }>(); - const config = useApplicationState().config; - const client = config.createClient(); const history = useHistory(); + const api = useApi(); return ( { - await client.api.patch("/auth/account/reset_password", { + await api.patch("/auth/account/reset_password", { token, ...data, }); diff --git a/src/pages/login/forms/FormVerify.tsx b/src/pages/login/forms/FormVerify.tsx index f7ecd1b9..32d0e4b4 100644 --- a/src/pages/login/forms/FormVerify.tsx +++ b/src/pages/login/forms/FormVerify.tsx @@ -1,26 +1,23 @@ import { useHistory, useParams } from "react-router-dom"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; -import { useApplicationState } from "../../../mobx/State"; +import { Category, Preloader } from "@revoltchat/ui"; -import { AppContext } from "../../../context/revoltjs/RevoltClient"; -import { takeError } from "../../../context/revoltjs/util"; - -import Overline from "../../../components/ui/Overline"; -import Preloader from "../../../components/ui/Preloader"; +import { I18nError } from "../../../context/Locale"; +import { useApi } from "../../../controllers/client/ClientController"; +import { takeError } from "../../../controllers/client/jsx/error"; import { Form } from "./Form"; export function FormResend() { - const config = useApplicationState().config; - const client = config.createClient(); + const api = useApi(); return ( { - await client.api.post("/auth/account/reverify", data); + await api.post("/auth/account/reverify", data); }} /> ); @@ -29,20 +26,20 @@ export function FormResend() { export function FormVerify() { const [error, setError] = useState(undefined); const { token } = useParams<{ token: string }>(); - const config = useApplicationState().config; - const client = config.createClient(); const history = useHistory(); + const api = useApi(); useEffect(() => { - client.api - .post(`/auth/account/verify/${token as ""}`) + api.post(`/auth/account/verify/${token as ""}`) .then(() => history.push("/login")) .catch((err) => setError(takeError(err))); // eslint-disable-next-line }, []); return error ? ( - + + + ) : ( ); diff --git a/src/pages/login/forms/MailProvider.tsx b/src/pages/login/forms/MailProvider.tsx index 70a7f9e3..ac384ebd 100644 --- a/src/pages/login/forms/MailProvider.tsx +++ b/src/pages/login/forms/MailProvider.tsx @@ -1,7 +1,7 @@ import styles from "../Login.module.scss"; import { Text } from "preact-i18n"; -import Button from "../../../components/ui/Button"; +import { Button } from "@revoltchat/ui"; interface Props { email?: string; diff --git a/src/pages/settings/ChannelSettings.tsx b/src/pages/settings/ChannelSettings.tsx index f25a557f..3910af62 100644 --- a/src/pages/settings/ChannelSettings.tsx +++ b/src/pages/settings/ChannelSettings.tsx @@ -4,11 +4,8 @@ import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Text } from "preact-i18n"; -import { useClient } from "../../context/revoltjs/RevoltClient"; -import { getChannelName } from "../../context/revoltjs/util"; - -import Category from "../../components/ui/Category"; - +import { useClient } from "../../controllers/client/ClientController"; +import { ChannelName } from "../../controllers/client/jsx/ChannelName"; import { GenericSettings } from "./GenericSettings"; import Overview from "./channel/Overview"; import Permissions from "./channel/Permissions"; @@ -49,7 +46,7 @@ export default function ChannelSettings() { {getChannelName(channel, true)}
, + category: , id: "overview", icon: , title: ( diff --git a/src/pages/settings/GenericSettings.tsx b/src/pages/settings/GenericSettings.tsx index dc665d30..ce2ff32f 100644 --- a/src/pages/settings/GenericSettings.tsx +++ b/src/pages/settings/GenericSettings.tsx @@ -5,25 +5,16 @@ import { useHistory, useParams } from "react-router-dom"; import styles from "./Settings.module.scss"; import classNames from "classnames"; import { Text } from "preact-i18n"; -import { - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "preact/hooks"; +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; + +import { Category, Header, IconButton, LineDivider } from "@revoltchat/ui"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; -import Category from "../../components/ui/Category"; -import Header from "../../components/ui/Header"; -import IconButton from "../../components/ui/IconButton"; -import LineDivider from "../../components/ui/LineDivider"; - import ButtonItem from "../../components/navigation/items/ButtonItem"; -import { Children } from "../../types/Preact"; +import { modalController } from "../../controllers/modals/ModalController"; interface Props { pages: { @@ -71,6 +62,8 @@ export function GenericSettings({ useEffect(() => { function keyDown(e: KeyboardEvent) { if (e.key === "Escape") { + if (modalController.isVisible) return; + exitSettings(); } } @@ -99,7 +92,7 @@ export function GenericSettings({ /> {isTouchscreenDevice && ( -
+
{typeof page === "undefined" ? ( <> {showExitButton && ( @@ -140,10 +133,9 @@ export function GenericSettings({ entry.hidden ? undefined : ( <> {entry.category && ( - + + {entry.category} + )} {entry.icon} {entry.title} - {entry.divider && } + {entry.divider && ( + + )} ), )} diff --git a/src/pages/settings/ServerSettings.tsx b/src/pages/settings/ServerSettings.tsx index 61a34448..6cd688a7 100644 --- a/src/pages/settings/ServerSettings.tsx +++ b/src/pages/settings/ServerSettings.tsx @@ -6,6 +6,7 @@ import { Envelope, UserX, Trash, + HappyBeaming, } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; import { Route, Switch, useHistory, useParams } from "react-router-dom"; @@ -13,24 +14,24 @@ import { Route, Switch, useHistory, useParams } from "react-router-dom"; import styles from "./Settings.module.scss"; import { Text } from "preact-i18n"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; -import RequiresOnline from "../../context/revoltjs/RequiresOnline"; -import { useClient } from "../../context/revoltjs/RevoltClient"; +import { LineDivider } from "@revoltchat/ui"; -import Category from "../../components/ui/Category"; -import LineDivider from "../../components/ui/LineDivider"; +import { state } from "../../mobx/State"; import ButtonItem from "../../components/navigation/items/ButtonItem"; +import { useClient } from "../../controllers/client/ClientController"; +import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; +import { modalController } from "../../controllers/modals/ModalController"; import { GenericSettings } from "./GenericSettings"; import { Bans } from "./server/Bans"; import { Categories } from "./server/Categories"; +import { Emojis } from "./server/Emojis"; import { Invites } from "./server/Invites"; import { Members } from "./server/Members"; import { Overview } from "./server/Overview"; import { Roles } from "./server/Roles"; export default observer(() => { - const { openScreen } = useIntermediate(); const { server: sid } = useParams<{ server: string }>(); const client = useClient(); const server = client.servers.get(sid); @@ -71,6 +72,15 @@ export default observer(() => { title: , hideTitle: true, }, + { + category: ( + + ), + id: "emojis", + icon: , + title: , + hidden: !state.experiments.isEnabled("picker"), + }, { category: ( @@ -119,6 +129,11 @@ export default observer(() => { + + + + + @@ -133,8 +148,7 @@ export default observer(() => { - openScreen({ - id: "special_prompt", + modalController.push({ type: "delete_server", target: server, }) diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index a3a0619d..18566e5c 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -4,6 +4,7 @@ import { Globe, LogOut, Desktop, + ListUl, } from "@styled-icons/boxicons-regular"; import { Bell, @@ -27,20 +28,21 @@ import styled from "styled-components/macro"; import styles from "./Settings.module.scss"; import { openContextMenu } from "preact-context-menu"; import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; + +import { LineDivider } from "@revoltchat/ui"; import { useApplicationState } from "../../mobx/State"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; -import RequiresOnline from "../../context/revoltjs/RequiresOnline"; -import { AppContext, LogOutContext } from "../../context/revoltjs/RevoltClient"; - import UserIcon from "../../components/common/user/UserIcon"; import { Username } from "../../components/common/user/UserShort"; import UserStatus from "../../components/common/user/UserStatus"; -import LineDivider from "../../components/ui/LineDivider"; - import ButtonItem from "../../components/navigation/items/ButtonItem"; +import { + useClient, + clientController, +} from "../../controllers/client/ClientController"; +import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; +import { modalController } from "../../controllers/modals/ModalController"; import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../revision"; import { APP_VERSION } from "../../version"; import { GenericSettings } from "./GenericSettings"; @@ -116,9 +118,7 @@ const AccountHeader = styled.div` export default observer(() => { const history = useHistory(); - const client = useContext(AppContext); - const logout = useContext(LogOutContext); - const { openScreen } = useIntermediate(); + const client = useClient(); const experiments = useApplicationState().experiments; function switchPage(to?: string) { @@ -258,6 +258,14 @@ export default observer(() => { category="pages" custom={ <> + + modalController.push({ type: "changelog" }) + }> + + + { - + @@ -336,9 +344,8 @@ export default observer(() => { - openScreen({ - id: "special_input", - type: "set_custom_status", + modalController.push({ + type: "custom_status", }) }> Change your status... diff --git a/src/pages/settings/assets/flags/brittany.svg b/src/pages/settings/assets/flags/brittany.svg new file mode 100644 index 00000000..7376601d --- /dev/null +++ b/src/pages/settings/assets/flags/brittany.svg @@ -0,0 +1,161 @@ + + + +Created with Fabric.js 3.6.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/settings/assets/enchanting_table.webp b/src/pages/settings/assets/flags/enchanting_table.webp similarity index 100% rename from src/pages/settings/assets/enchanting_table.webp rename to src/pages/settings/assets/flags/enchanting_table.webp diff --git a/src/pages/settings/assets/esperanto.svg b/src/pages/settings/assets/flags/esperanto.svg similarity index 100% rename from src/pages/settings/assets/esperanto.svg rename to src/pages/settings/assets/flags/esperanto.svg diff --git a/src/pages/settings/assets/flags/kurdistan.svg b/src/pages/settings/assets/flags/kurdistan.svg new file mode 100644 index 00000000..11037b91 --- /dev/null +++ b/src/pages/settings/assets/flags/kurdistan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/settings/assets/flags/sources.txt b/src/pages/settings/assets/flags/sources.txt new file mode 100644 index 00000000..98f6c95e --- /dev/null +++ b/src/pages/settings/assets/flags/sources.txt @@ -0,0 +1,27 @@ +Flag of Brittany +CC BY-SA 4.0 +https://commons.wikimedia.org/wiki/File:Flag_of_Brittany.svg + +Enchanting Table +Minecraft game render +https://minecraft.fandom.com/wiki/Enchanting_Table?file=Enchanting_Table.gif + +Flag of Esperanto +Public Domain +https://commons.wikimedia.org/wiki/File:Flag_of_Esperanto.svg + +Flag of Kurdistan +Public Domain +https://commons.wikimedia.org/wiki/File:Flag_of_Kurdistan.svg + +Tamil Nadu Flag +CC BY-SA 3.0 +https://commons.wikimedia.org/wiki/File:..Tamil_Nadu_Flag(INDIA).png + +Toki Pona Flag +Free for any use +https://www.reddit.com/r/tokipona/comments/mevzbn/a_flag_for_toki_pona/gsk3euc/ + +Flag of Veneto +CC BY-SA 3.0 +https://commons.wikimedia.org/wiki/File:Flag_of_Veneto.svg diff --git a/src/pages/settings/assets/tamil_nadu_flag.png b/src/pages/settings/assets/flags/tamil_nadu.png similarity index 100% rename from src/pages/settings/assets/tamil_nadu_flag.png rename to src/pages/settings/assets/flags/tamil_nadu.png diff --git a/src/pages/settings/assets/flags/toki_pona.svg b/src/pages/settings/assets/flags/toki_pona.svg new file mode 100644 index 00000000..371c37b9 --- /dev/null +++ b/src/pages/settings/assets/flags/toki_pona.svg @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/src/pages/settings/assets/flags/veneto.svg b/src/pages/settings/assets/flags/veneto.svg new file mode 100644 index 00000000..1afb7ec1 --- /dev/null +++ b/src/pages/settings/assets/flags/veneto.svg @@ -0,0 +1,499 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PAX + tibi + mar + ce e + van + geli + sta + mevs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/settings/assets/toki_pona.svg b/src/pages/settings/assets/toki_pona.svg deleted file mode 100644 index e29c05f6..00000000 --- a/src/pages/settings/assets/toki_pona.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - -Created by potrace 1.5, written by Peter Selinger 2001-2004 - - - - - - - - - - \ No newline at end of file diff --git a/src/pages/settings/channel/Overview.tsx b/src/pages/settings/channel/Overview.tsx index e728f571..392fb216 100644 --- a/src/pages/settings/channel/Overview.tsx +++ b/src/pages/settings/channel/Overview.tsx @@ -5,13 +5,11 @@ import styled from "styled-components/macro"; import { Text } from "preact-i18n"; import { useEffect, useState } from "preact/hooks"; +import { Button, Checkbox, InputBox } from "@revoltchat/ui"; + import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; -import { FileUploader } from "../../../context/revoltjs/FileUploads"; - -import Button from "../../../components/ui/Button"; -import Checkbox from "../../../components/ui/Checkbox"; -import InputBox from "../../../components/ui/InputBox"; +import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads"; interface Props { channel: Channel; @@ -85,7 +83,7 @@ export default observer(({ channel }: Props) => { )} { @@ -119,17 +117,17 @@ export default observer(({ channel }: Props) => { "" ) : ( { setNSFW(nsfwchange); if (!changed) setChanged(true); }} - description="Set this channel to NSFW."> - NSFW - + title="NSFW" + description="Set this channel to NSFW." + /> )}

-

diff --git a/src/pages/settings/channel/Permissions.tsx b/src/pages/settings/channel/Permissions.tsx index 662d3bcc..d728106f 100644 --- a/src/pages/settings/channel/Permissions.tsx +++ b/src/pages/settings/channel/Permissions.tsx @@ -101,7 +101,7 @@ export default observer(({ channel }: Props) => { filter={[ ...(channel.channel_type === "Group" ? [] - : ["ViewChannel" as "ViewChannel"]), + : ["ViewChannel" as const]), "ReadMessageHistory", "SendMessage", "ManageMessages", diff --git a/src/pages/settings/panes/Account.tsx b/src/pages/settings/panes/Account.tsx index 08d4722c..dd6f76ca 100644 --- a/src/pages/settings/panes/Account.tsx +++ b/src/pages/settings/panes/Account.tsx @@ -1,237 +1,34 @@ -import { At, Key, Block } from "@styled-icons/boxicons-regular"; -import { - Envelope, - HelpCircle, - Lock, - Trash, - Pencil, -} from "@styled-icons/boxicons-solid"; -import { observer } from "mobx-react-lite"; -import { useHistory } from "react-router-dom"; -import { API } from "revolt.js"; +import { Link } from "react-router-dom"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; -import { useContext, useEffect, useState } from "preact/hooks"; -import { stopPropagation } from "../../../lib/stopPropagation"; +import { Tip } from "@revoltchat/ui"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { - ClientStatus, - StatusContext, - useClient, -} from "../../../context/revoltjs/RevoltClient"; - -import Tooltip from "../../../components/common/Tooltip"; -import UserIcon from "../../../components/common/user/UserIcon"; -import Button from "../../../components/ui/Button"; -import Tip from "../../../components/ui/Tip"; -import CategoryButton from "../../../components/ui/fluent/CategoryButton"; - -export const Account = observer(() => { - const { openScreen, writeClipboard } = useIntermediate(); - const status = useContext(StatusContext); - - const client = useClient(); - - const [email, setEmail] = useState("..."); - const [revealEmail, setRevealEmail] = useState(false); - const [profile, setProfile] = useState( - undefined, - ); - const history = useHistory(); - - function switchPage(to: string) { - history.replace(`/settings/${to}`); - } - - useEffect(() => { - if (email === "..." && status === ClientStatus.ONLINE) { - client.api - .get("/auth/account/") - .then((account) => setEmail(account.email)); - } - - if (profile === undefined && status === ClientStatus.ONLINE) { - client - .user!.fetchProfile() - .then((profile) => setProfile(profile ?? {})); - } - }, [client, email, profile, status]); +import AccountManagement from "../../../components/settings/account/AccountManagement"; +import EditAccount from "../../../components/settings/account/EditAccount"; +import MultiFactorAuthentication from "../../../components/settings/account/MultiFactorAuthentication"; +export function Account() { return (
- -
- {( - [ - [ - "username", - client.user!.username, - , - ], - ["email", email, ], - ["password", "•••••••••", ], - ] as const - ).map(([field, value, icon]) => ( - - {value}{" "} - - stopPropagation( - ev, - setRevealEmail(false), - ) - }> - - - - ) : ( - <> - •••••••••••@••••••.•••{" "} - - stopPropagation( - ev, - setRevealEmail(true), - ) - }> - - - - ) - ) : ( - value - ) - } - account - action={} - onClick={() => - openScreen({ - id: "modify_account", - field, - }) - }> - - - ))} -
+
-

- -

-
- {/**/} - Two-factor authentication is currently work-in-progress, see{" "} - {` `} - - v1 milestone here - - . -
- } - description={"Set up 2FA on your account."} - disabled - action={}> - Set up Two-factor authentication - - {/*} - description={"View and download your 2FA backup codes."} - disabled - action="chevron"> - View my backup codes - */} + +
-

- -

-
- -
- } - description={ - "Disable your account. You won't be able to access it unless you log back in." - } - disabled - action={}> - - - - } - description={ - "Delete your account, including all of your data. (sends an email to contact@revolt.chat)" - } - hover - action="external"> - - - + {" "} - switchPage("profile")}> + - +
); -}); +} diff --git a/src/pages/settings/panes/Appearance.tsx b/src/pages/settings/panes/Appearance.tsx index a38c9cb8..d964f187 100644 --- a/src/pages/settings/panes/Appearance.tsx +++ b/src/pages/settings/panes/Appearance.tsx @@ -4,55 +4,32 @@ import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import CollapsibleSection from "../../../components/common/CollapsibleSection"; - -import { - ThemeBaseSelectorShim, - ThemeShopShim, - ThemeAccentShim, - DisplayFontShim, - DisplayMonospaceFontShim, - DisplayLigaturesShim, - DisplayEmojiShim, - ThemeCustomCSSShim, - DisplaySeasonalShim, - DisplayTransparencyShim, -} from "../../../components/settings/AppearanceShims"; +import AdvancedOptions from "../../../components/settings/appearance/AdvancedOptions"; +import AppearanceOptions from "../../../components/settings/appearance/AppearanceOptions"; +import ChatOptions from "../../../components/settings/appearance/ChatOptions"; import ThemeOverrides from "../../../components/settings/appearance/ThemeOverrides"; -import ThemeTools from "../../../components/settings/appearance/ThemeTools"; +import ThemeSelection from "../../../components/settings/appearance/ThemeSelection"; export const Appearance = observer(() => { return (
- - +
- +
-

- -

- - -
- - -
- +
}> - -

App

}> - - +
); diff --git a/src/pages/settings/panes/Audio.tsx b/src/pages/settings/panes/Audio.tsx index 1e39185b..093d83cd 100644 --- a/src/pages/settings/panes/Audio.tsx +++ b/src/pages/settings/panes/Audio.tsx @@ -2,14 +2,13 @@ import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useEffect, useState } from "preact/hooks"; -import { TextReact } from "../../../lib/i18n"; +import { Button, Category, ComboBox, Tip } from "@revoltchat/ui"; + import { stopPropagation } from "../../../lib/stopPropagation"; import { voiceState } from "../../../lib/vortex/VoiceState"; -import Button from "../../../components/ui/Button"; -import ComboBox from "../../../components/ui/ComboBox"; -import Overline from "../../../components/ui/Overline"; -import Tip from "../../../components/ui/Tip"; +import { I18nError } from "../../../context/Locale"; + import opusSVG from "../assets/opus_logo.svg"; { @@ -94,15 +93,15 @@ export function Audio() { return ( <> -
+
{!permission && ( - + )} {error && permission === "prompt" && ( - + @@ -116,7 +115,7 @@ export function Audio() {

-
+
handleAskForPermission(e)} - error> + onClick={(e: any) => + handleAskForPermission(e) + } + palette="error"> )} {error && error.name === "NotAllowedError" && ( - + + + )}
diff --git a/src/pages/settings/panes/Experiments.tsx b/src/pages/settings/panes/Experiments.tsx index b91e756f..8f36f7bd 100644 --- a/src/pages/settings/panes/Experiments.tsx +++ b/src/pages/settings/panes/Experiments.tsx @@ -3,14 +3,14 @@ import { observer } from "mobx-react-lite"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; +import { Checkbox, Column } from "@revoltchat/ui"; + import { useApplicationState } from "../../../mobx/State"; import { AVAILABLE_EXPERIMENTS, EXPERIMENTS, } from "../../../mobx/stores/Experiments"; -import Checkbox from "../../../components/ui/Checkbox"; - export const ExperimentsPage = observer(() => { const experiments = useApplicationState().experiments; @@ -19,15 +19,19 @@ export const ExperimentsPage = observer(() => {

- {AVAILABLE_EXPERIMENTS.map((key) => ( - experiments.setEnabled(key, enabled)} - description={EXPERIMENTS[key].description}> - {EXPERIMENTS[key].title} - - ))} + + {AVAILABLE_EXPERIMENTS.map((key) => ( + + experiments.setEnabled(key, enabled) + } + description={EXPERIMENTS[key].description} + title={EXPERIMENTS[key].title} + /> + ))} + {AVAILABLE_EXPERIMENTS.length === 0 && (
diff --git a/src/pages/settings/panes/Feedback.tsx b/src/pages/settings/panes/Feedback.tsx index 106159db..66125beb 100644 --- a/src/pages/settings/panes/Feedback.tsx +++ b/src/pages/settings/panes/Feedback.tsx @@ -5,7 +5,7 @@ import { Link } from "react-router-dom"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; -import CategoryButton from "../../../components/ui/fluent/CategoryButton"; +import { CategoryButton } from "@revoltchat/ui"; export function Feedback() { return ( @@ -15,7 +15,6 @@ export function Feedback() { target="_blank" rel="noreferrer"> } description={ @@ -29,7 +28,6 @@ export function Feedback() { target="_blank" rel="noreferrer"> } description={ @@ -43,7 +41,6 @@ export function Feedback() { target="_blank" rel="noreferrer"> } description={ @@ -55,7 +52,6 @@ export function Feedback() { } description="You can report issues and discuss improvements with us directly here."> diff --git a/src/pages/settings/panes/Languages.tsx b/src/pages/settings/panes/Languages.tsx index 7ea9d283..c347de52 100644 --- a/src/pages/settings/panes/Languages.tsx +++ b/src/pages/settings/panes/Languages.tsx @@ -1,24 +1,28 @@ +import { Check } from "@styled-icons/boxicons-regular"; import { observer } from "mobx-react-lite"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useMemo } from "preact/hooks"; +import { Checkbox, LineDivider, Tip } from "@revoltchat/ui"; + import { useApplicationState } from "../../../mobx/State"; -import Emoji from "../../../components/common/Emoji"; -import Checkbox from "../../../components/ui/Checkbox"; -import Tip from "../../../components/ui/Tip"; -import enchantingTableWEBP from "../assets/enchanting_table.webp"; -import esperantoFlagSVG from "../assets/esperanto.svg"; -import tamilFlagPNG from "../assets/tamil_nadu_flag.png"; -import tokiponaSVG from "../assets/toki_pona.svg"; +import britannyFlagSVG from "../assets/flags/brittany.svg"; +import enchantingTableWEBP from "../assets/flags/enchanting_table.webp"; +import esperantoFlagSVG from "../assets/flags/esperanto.svg"; +import kurdistanFlagSVG from "../assets/flags/kurdistan.svg"; +import tamilFlagPNG from "../assets/flags/tamil_nadu.png"; +import tokiponaSVG from "../assets/flags/toki_pona.svg"; +import venetoFlagSVG from "../assets/flags/veneto.svg"; import { Language, LanguageEntry, Languages as Langs, } from "../../../../external/lang/Languages"; +import Emoji from "../../../components/common/Emoji"; type Key = [Language, LanguageEntry]; @@ -37,38 +41,82 @@ function Entry({ entry: [x, lang], selected, onSelect }: Props) { -
- {lang.i18n === "eo" ? ( - - ) : lang.i18n === "ta" ? ( - - ) : lang.emoji === "🙂" ? ( - - ) : lang.emoji === "🪄" ? ( - - ) : ( - - )} -
- {lang.display} -
+ value={selected} + onChange={onSelect} + title={ + <> +
+ {lang.i18n === "vec" ? ( + + ) : lang.i18n === "br" ? ( + + ) : lang.i18n === "ckb" ? ( + + ) : lang.i18n === "eo" ? ( + + ) : lang.i18n === "ta" ? ( + + ) : lang.emoji === "🙂" ? ( + + ) : lang.emoji === "🪄" ? ( + + ) : ( + + )} +
+ + {lang.display} {lang.verified && } + + + } + /> ); } @@ -149,16 +197,17 @@ export const Languages = observer(() => { .filter(([, lang]) => lang.cat === "alt") .map(EntryFactory)}
+ - - {" "} - - - + {" "} + + + +
); diff --git a/src/pages/settings/panes/MyBots.tsx b/src/pages/settings/panes/MyBots.tsx index 861b5f33..03af49c0 100644 --- a/src/pages/settings/panes/MyBots.tsx +++ b/src/pages/settings/panes/MyBots.tsx @@ -3,34 +3,35 @@ import { Key, Clipboard, Globe, Plus } from "@styled-icons/boxicons-regular"; import { LockAlt, HelpCircle } from "@styled-icons/boxicons-solid"; import type { AxiosError } from "axios"; import { observer } from "mobx-react-lite"; -import { API } from "revolt.js"; -import { User } from "revolt.js"; +import { API, User } from "revolt.js"; import styled from "styled-components/macro"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useCallback, useEffect, useState } from "preact/hooks"; +import { + Button, + CategoryButton, + Checkbox, + InputBox, + Tip, +} from "@revoltchat/ui"; + import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import { internalEmit } from "../../../lib/eventEmitter"; import { useTranslation } from "../../../lib/i18n"; import { stopPropagation } from "../../../lib/stopPropagation"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { FileUploader } from "../../../context/revoltjs/FileUploads"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - import AutoComplete, { useAutoComplete, } from "../../../components/common/AutoComplete"; import CollapsibleSection from "../../../components/common/CollapsibleSection"; import Tooltip from "../../../components/common/Tooltip"; import UserIcon from "../../../components/common/user/UserIcon"; -import Button from "../../../components/ui/Button"; -import Checkbox from "../../../components/ui/Checkbox"; -import InputBox from "../../../components/ui/InputBox"; -import Tip from "../../../components/ui/Tip"; -import CategoryButton from "../../../components/ui/fluent/CategoryButton"; +import { useClient } from "../../../controllers/client/ClientController"; +import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads"; +import { modalController } from "../../../controllers/modals/ModalController"; interface Data { _id: string; @@ -85,7 +86,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { ); const [interactionsRef, setInteractionsRef] = useState(null); - const { writeClipboard, openScreen } = useIntermediate(); const [profile, setProfile] = useState( undefined, @@ -95,12 +95,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { client.api .get(`/users/${bot._id as ""}/profile`, undefined, { headers: { "x-bot-token": bot.token }, - transformRequest: (data, headers) => { - // Remove user headers for this request - delete headers?.["x-user-id"]; - delete headers?.["x-session-token"]; - return data; - }, }) .then((profile) => setProfile(profile ?? {})); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -126,7 +120,8 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { setSaving(true); setError(""); try { - await client.bots.edit(bot._id, changes); + if (Object.keys(changes).length > 0) + await client.bots.edit(bot._id, changes); if (changed) await editBotContent(profile?.content ?? undefined); onUpdate(changes); setChanged(false); @@ -155,12 +150,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { avatar ? { avatar } : { remove: ["Avatar"] }, { headers: { "x-bot-token": bot.token }, - transformRequest: (data, headers) => { - // Remove user headers for this request - delete headers?.["x-user-id"]; - delete headers?.["x-session-token"]; - return data; - }, }, ); @@ -180,12 +169,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { : { remove: ["ProfileBackground"] }, { headers: { "x-bot-token": bot.token }, - transformRequest: (data, headers) => { - // Remove user headers for this request - delete headers?.["x-user-id"]; - delete headers?.["x-session-token"]; - return data; - }, }, ); @@ -202,12 +185,6 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { content ? { profile: { content } } : { remove: ["ProfileContent"] }, { headers: { "x-bot-token": bot.token }, - transformRequest: (data, headers) => { - // Remove user headers for this request - delete headers?.["x-user-id"]; - delete headers?.["x-session-token"]; - return data; - }, }, ); @@ -238,8 +215,8 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { target={user} size={42} onClick={() => - openScreen({ - id: "profile", + modalController.push({ + type: "user_profile", user_id: user._id, }) } @@ -287,7 +264,9 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { }> - writeClipboard(user!._id) + modalController.writeText( + user!._id, + ) }> {user!._id} @@ -296,6 +275,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
) : ( + palette="secondary"> } - onClick={() => writeClipboard(bot.token)} + onClick={() => modalController.writeText(bot.token)} description={ <> {"••••• "} @@ -362,10 +342,10 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { onClick={(ev) => stopPropagation( ev, - openScreen({ - id: "token_reveal", + modalController.push({ + type: "show_token", token: bot.token, - username: user!.username, + name: user!.username, }), ) }> @@ -434,15 +414,14 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { /> } description={ } - onChange={(v) => setData({ ...data, public: v })}> - - + onChange={(v) => setData({ ...data, public: v })} + />

@@ -450,6 +429,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) { - - {error} - + {error}
)} @@ -478,11 +456,10 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
); }); diff --git a/src/pages/settings/panes/Panes.module.scss b/src/pages/settings/panes/Panes.module.scss index ff5c3321..e5faf987 100644 --- a/src/pages/settings/panes/Panes.module.scss +++ b/src/pages/settings/panes/Panes.module.scss @@ -128,8 +128,9 @@ border-radius: var(--border-radius); width: 100%; display: flex; - display: grid; - place-items: center; + flex-direction: column; + align-items: center; + justify-content: center; grid-template-columns: minmax(auto, 100%); > div { @@ -480,12 +481,14 @@ border-radius: var(--border-radius); margin-top: 0; + transition: 0.1s ease background-color; + &:hover { background: var(--secondary-background); } } - .entry > span > span { + .entry > div div { gap: 8px; display: flex; align-items: center; diff --git a/src/pages/settings/panes/Plugins.tsx b/src/pages/settings/panes/Plugins.tsx index c33e2e6c..51b13f50 100644 --- a/src/pages/settings/panes/Plugins.tsx +++ b/src/pages/settings/panes/Plugins.tsx @@ -1,15 +1,11 @@ -import { Check } from "@styled-icons/boxicons-regular"; import { observer } from "mobx-react-lite"; -import styled from "styled-components"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; -import { useApplicationState } from "../../../mobx/State"; +import { Button, Checkbox, Tip } from "@revoltchat/ui"; -import Button from "../../../components/ui/Button"; -import { CheckboxBase, Checkmark } from "../../../components/ui/Checkbox"; -import Tip from "../../../components/ui/Tip"; +import { useApplicationState } from "../../../mobx/State"; // Just keeping this here for general purpose. Should probably be exported // elsewhere, though. @@ -20,45 +16,10 @@ interface Plugin { enabled: boolean | undefined; } -const CustomCheckboxBase = styled(CheckboxBase)` - margin-top: 0 !important; -`; -export interface CheckboxProps { - checked: boolean; - disabled?: boolean; - onChange: (state: boolean) => void; -} -function PluginCheckbox(props: CheckboxProps) { - // HACK HACK HACK(lexisother): THIS ENTIRE THING IS A HACK!!!! - /* - Until some reviewer points me in the right direction, I've resorted to - fabricating my own checkbox component. - "WHY?!", you might ask. Well, the normal `Checkbox` component can take - textual contents, and *also* adds a `margin-top` of 20 pixels. - We... don't need that. At all. *Especially* the margin. It makes our card - look disproportionate. - - Apologies, @insert! - */ - return ( - - - !props.disabled && props.onChange(!props.checked) - } - /> - - - - - ); -} - interface CardProps { plugin: Plugin; } + function PluginCard({ plugin }: CardProps) { const plugins = useApplicationState().plugins; @@ -69,12 +30,14 @@ function PluginCard({ plugin }: CardProps) {
-
- {plugin.namespace} / {plugin.id} -
- + {plugin.namespace} / {plugin.id} + + } onChange={() => { !plugin.enabled ? plugins.load(plugin.namespace, plugin.id) @@ -88,7 +51,7 @@ function PluginCard({ plugin }: CardProps) {

+ + - Want to change your username?{" "} - switchPage("account")}> - Head over to your account settings. - + + Want to change your username?{" "} + switchPage("account")}> + Head over to your account settings. + +
); diff --git a/src/pages/settings/panes/Sessions.tsx b/src/pages/settings/panes/Sessions.tsx index f3c2134f..d3f6e13c 100644 --- a/src/pages/settings/panes/Sessions.tsx +++ b/src/pages/settings/panes/Sessions.tsx @@ -16,21 +16,25 @@ import { decodeTime } from "ulid"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; + +import { + Button, + CategoryButton, + LineDivider, + Preloader, + Tip, +} from "@revoltchat/ui"; import { dayjs } from "../../../context/Locale"; -import { AppContext } from "../../../context/revoltjs/RevoltClient"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import Button from "../../../components/ui/Button"; -import Preloader from "../../../components/ui/Preloader"; -import Tip from "../../../components/ui/Tip"; -import CategoryButton from "../../../components/ui/fluent/CategoryButton"; +import { useClient } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; dayjs.extend(relativeTime); export function Sessions() { - const client = useContext(AppContext); + const client = useClient(); const deviceId = typeof client.session === "object" ? client.session._id : undefined; @@ -40,8 +44,6 @@ export function Sessions() { const [attemptingDelete, setDelete] = useState([]); const history = useHistory(); - const { openScreen } = useIntermediate(); - function switchPage(to: string) { history.replace(`/settings/${to}`); } @@ -168,7 +170,7 @@ export function Sessions() { type="text" className={styles.name} value={session.name} - autocomplete="off" + autoComplete="off" style={{ pointerEvents: "none" }} /> @@ -214,28 +216,22 @@ export function Sessions() { })}
{ - openScreen({ - id: "sessions", - confirm: async () => { - // ! FIXME: add to rAuth - const del: string[] = []; - render.forEach((session) => { - if (deviceId !== session._id) { - del.push(session._id); - } - }); - - setDelete(del); - - for (const id of del) { - await client.api.delete(`/auth/session/${id as ""}`); - } - - setSessions(sessions.filter((x) => x._id === deviceId)); - } + onClick={async () => + modalController.push({ + type: "sign_out_sessions", + client, + onDeleting: () => + setDelete( + render + .filter((x) => x._id !== deviceId) + .map((x) => x._id), + ), + onDelete: () => + setSessions( + sessions.filter((x) => x._id === deviceId), + ), }) - }} + } icon={} action={"chevron"} description={ @@ -244,15 +240,15 @@ export function Sessions() { + - - {" "} - switchPage("account")}> - - + {" "} + switchPage("account")}> + + +
); } - diff --git a/src/pages/settings/panes/Sync.tsx b/src/pages/settings/panes/Sync.tsx index f426c4d4..c068634f 100644 --- a/src/pages/settings/panes/Sync.tsx +++ b/src/pages/settings/panes/Sync.tsx @@ -3,11 +3,11 @@ import { observer } from "mobx-react-lite"; import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; +import { Checkbox, Column } from "@revoltchat/ui"; + import { useApplicationState } from "../../../mobx/State"; import { SyncKeys } from "../../../mobx/stores/Sync"; -import Checkbox from "../../../components/ui/Checkbox"; - export const Sync = observer(() => { const sync = useApplicationState().sync; @@ -20,26 +20,28 @@ export const Sync = observer(() => {

- {( - [ - ["appearance", "appearance.title"], - ["theme", "appearance.theme"], - ["locale", "language.title"], - // notifications sync is always-on - ] as [SyncKeys, string][] - ).map(([key, title]) => ( - - } - onChange={() => sync.toggle(key)}> - - - ))} + + {( + [ + ["appearance", "appearance.title"], + ["theme", "appearance.theme"], + ["locale", "language.title"], + // notifications sync is always-on + ] as [SyncKeys, string][] + ).map(([key, title]) => ( + } + description={ + + } + onChange={() => sync.toggle(key)} + /> + ))} + {/*
Last sync at 12:00
*/} diff --git a/src/pages/settings/panes/ThemeShop.tsx b/src/pages/settings/panes/ThemeShop.tsx deleted file mode 100644 index ea010736..00000000 --- a/src/pages/settings/panes/ThemeShop.tsx +++ /dev/null @@ -1,399 +0,0 @@ -/** - * ! DEPRECATED FILE - * ! DO NOT IMPORT - * - * Replaced by Revolt Discover - */ -import { Check } from "@styled-icons/boxicons-regular"; -import { - Star, - Brush, - Bookmark, - BarChartAlt2, -} from "@styled-icons/boxicons-solid"; -import styled from "styled-components/macro"; - -import { Text } from "preact-i18n"; -import { useEffect, useState } from "preact/hooks"; - -import { useApplicationState } from "../../../mobx/State"; - -import { Theme, generateVariables } from "../../../context/Theme"; - -import Tip from "../../../components/ui/Tip"; -import previewPath from "../assets/preview.svg"; - -import { GIT_REVISION } from "../../../revision"; - -export const fetchManifest = (): Promise => - fetch(`${import.meta.env.VITE_THEMES_URL}/manifest.json`).then((res) => - res.json(), - ); - -export const fetchTheme = (slug: string): Promise => - fetch(`${import.meta.env.VITE_THEMES_URL}/theme_${slug}.json`).then((res) => - res.json(), - ); - -export interface ThemeMetadata { - name: string; - creator: string; - commit?: string; - description: string; -} - -export type Manifest = { - generated: string; - themes: Record; -}; - -// TODO: ability to preview / display the settings set like in the appearance pane -const ThemeInfo = styled.article` - display: flex; - flex-direction: column; - gap: 10px; - padding: 1rem; - border-radius: var(--border-radius); - background: var(--secondary-background); - - &[data-loaded] { - .preview { - opacity: 1; - } - } - - .preview { - grid-area: preview; - aspect-ratio: 323 / 202; - - background-color: var(--secondary-background); - border-radius: calc(var(--border-radius) / 2); - - // prep style for later - outline: 3px solid transparent; - - // hide random svg parts, crop border on firefox - overflow: hidden; - - // hide until loaded - opacity: 0; - - // style button - border: 0; - margin: 0; - padding: 0; - - transition: 0.25s opacity, 0.25s outline; - - > * { - grid-area: 1 / 1; - } - - svg { - height: 100%; - width: 100%; - object-fit: contain; - } - - &:hover, - &:active, - &:focus-visible { - outline: 3px solid var(--tertiary-background); - } - } - - .name { - margin-top: 5px !important; - grid-area: name; - margin: 0; - } - - .creator { - grid-area: creator; - justify-self: end; - font-size: 0.75rem; - } - - .description { - margin-bottom: 5px; - grid-area: desc; - } - - .previewBox { - position: relative; - height: 100%; - width: 100%; - - .hover { - opacity: 0; - font-family: var(--font), sans-serif; - font-variant-ligatures: var(--ligatures); - font-weight: 600; - display: flex; - align-items: center; - justify-content: center; - color: white; - height: 100%; - width: 100%; - z-index: 10; - position: absolute; - background: rgba(0, 0, 0, 0.5); - cursor: pointer; - transition: opacity 0.2s ease-in-out; - - &:hover { - opacity: 1; - } - } - - > svg { - height: 100%; - } - } -`; - -const ThemeList = styled.div` - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 1rem; -`; - -const Banner = styled.div` - display: flex; - flex-direction: column; -`; - -const Category = styled.div` - display: flex; - gap: 8px; - align-items: center; - - .title { - display: flex; - align-items: center; - gap: 8px; - flex-grow: 1; - } - - .view { - font-size: 12px; - } -`; - -const ActiveTheme = styled.div` - display: flex; - flex-direction: column; - background: var(--secondary-background); - padding: 0; - border-radius: var(--border-radius); - gap: 8px; - overflow: hidden; - - .active-indicator { - display: flex; - gap: 6px; - align-items: center; - background: var(--accent); - width: 100%; - padding: 5px 10px; - font-size: 13px; - font-weight: 400; - color: white; - } - .title { - font-size: 1.2rem; - font-weight: 600; - } - - .author { - font-size: 12px; - margin-bottom: 5px; - } - - .theme { - width: 124px; - height: 80px; - background: var(--tertiary-background); - border-radius: 4px; - } - - .container { - display: flex; - gap: 16px; - padding: 10px 16px 16px; - } -`; - -const ThemedSVG = styled.svg<{ theme: Theme }>` - ${(props) => props.theme && generateVariables(props.theme)} -`; - -type ThemePreviewProps = Omit, "as"> & { - slug?: string; - theme?: Theme; - onThemeLoaded?: (theme: Theme) => void; -}; - -const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => { - return ( - - ); -}; - -const ThemeShopRoot = styled.div` - display: grid; - gap: 1rem; - - h5 { - margin-bottom: 0; - } -`; - -export function ThemeShop() { - // setThemeList is for adding more / lazy loading in the future - const [themeList, setThemeList] = useState< - [string, ThemeMetadata][] | null - >(null); - const [themeData, setThemeData] = useState>({}); - - const themes = useApplicationState().settings.theme; - - async function fetchThemeList() { - const manifest = await fetchManifest(); - setThemeList( - Object.entries(manifest.themes).filter((x) => - x[1].commit ? x[1].commit === GIT_REVISION : true, - ), - ); - } - - async function getTheme(slug: string) { - const theme = await fetchTheme(slug); - setThemeData((data) => ({ ...data, [slug]: theme })); - } - - useEffect(() => { - fetchThemeList(); - }, []); - - useEffect(() => { - themeList?.forEach(([slug]) => { - getTheme(slug); - }); - }, [themeList]); - - return ( - -
- -
- {/* -
- Oops! Couldn't load the theme shop. Make sure you're - connected to the internet and try again. -
-
*/} - - The Theme Shop is currently under construction. - - - {/* FIXME INTEGRATE WITH MOBX */} - {/* -
- - -
-
-
theme svg goes here
-
-
Theme Title
-
- {" "} - Author -
-
This is a theme description.
-
-
-
- " contrast /> - -
- - -
- - - -
- - -
- - -
- - - -
- - -
- - -
- - - -
- - -
- - -
- - - -
*/} -
- - {themeList?.map(([slug, theme]) => ( - - -

{theme.name}

- {/* Maybe id's of the users should be included as well / instead? */} -
- {" "} - {theme.creator} -
-
{theme.description}
-
- ))} -
-
- ); -} diff --git a/src/pages/settings/server/Bans.tsx b/src/pages/settings/server/Bans.tsx index a6fefeec..3896f6f6 100644 --- a/src/pages/settings/server/Bans.tsx +++ b/src/pages/settings/server/Bans.tsx @@ -8,10 +8,9 @@ import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useEffect, useMemo, useState } from "preact/hooks"; +import { IconButton, InputBox, Preloader } from "@revoltchat/ui"; + import UserIcon from "../../../components/common/user/UserIcon"; -import IconButton from "../../../components/ui/IconButton"; -import Preloader from "../../../components/ui/Preloader"; -import { InputBox } from "@revoltchat/ui"; interface InnerProps { ban: API.ServerBan; @@ -39,8 +38,7 @@ const Inner = observer(({ ban, users, server, removeSelf }: InnerProps) => { onClick={() => { setDelete(true); server.unbanUser(ban._id.user).then(removeSelf); - }} - disabled={deleting}> + }}>
@@ -101,7 +99,7 @@ export const Bans = observer(({ server }: Props) => { - + diff --git a/src/pages/settings/server/Categories.tsx b/src/pages/settings/server/Categories.tsx index 5dd9e74c..f9f4bc43 100644 --- a/src/pages/settings/server/Categories.tsx +++ b/src/pages/settings/server/Categories.tsx @@ -8,14 +8,14 @@ import { ulid } from "ulid"; import { Text } from "preact-i18n"; import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; +import { SaveStatus } from "@revoltchat/ui"; + import { useAutosave } from "../../../lib/debounce"; import { Draggable, Droppable } from "../../../lib/dnd"; import { noop } from "../../../lib/js"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - import ChannelIcon from "../../../components/common/ChannelIcon"; -import SaveStatus, { EditStatus } from "../../../components/ui/SaveStatus"; +import { modalController } from "../../../controllers/modals/ModalController"; const KanbanEntry = styled.div` padding: 2px 4px; @@ -132,7 +132,9 @@ interface Props { } export const Categories = observer(({ server }: Props) => { - const [status, setStatus] = useState("saved"); + const [status, setStatus] = useState<"saved" | "editing" | "saving">( + "saved", + ); const [categories, setCategories] = useState( server.categories ?? [], ); @@ -289,7 +291,7 @@ export const Categories = observer(({ server }: Props) => { /> ))} -
+
setCategories([ @@ -330,12 +332,9 @@ function ListElement({ index: number; setTitle?: (title: string) => void; deleteSelf?: () => void; - addChannel: ( - channel: Channel & { channel_type: "TextChannel" | "VoiceChannel" }, - ) => void; + addChannel: (channel: Channel) => void; draggable?: boolean; }) { - const { openScreen } = useIntermediate(); const [editing, setEditing] = useState(); const startEditing = () => setTitle && setEditing(category.title); @@ -366,7 +365,7 @@ function ListElement({ {(provided) => (
-
+
{editing ? ( @@ -419,7 +418,7 @@ function ListElement({ provided.innerRef }> -
+
- openScreen({ - id: "special_prompt", + modalController.push({ type: "create_channel", target: server, cb: addChannel, diff --git a/src/pages/settings/server/Emojis.tsx b/src/pages/settings/server/Emojis.tsx new file mode 100644 index 00000000..1395e7d2 --- /dev/null +++ b/src/pages/settings/server/Emojis.tsx @@ -0,0 +1,89 @@ +import { observer } from "mobx-react-lite"; +import { Server } from "revolt.js"; +import styled from "styled-components"; + +import { Text } from "preact-i18n"; + +import { Button, Column, Row, Stacked } from "@revoltchat/ui"; + +import UserShort from "../../../components/common/user/UserShort"; +import { EmojiUploader } from "../../../components/settings/customisation/EmojiUploader"; +import { modalController } from "../../../controllers/modals/ModalController"; + +interface Props { + server: Server; +} + +const List = styled.div` + gap: 8px; + display: flex; + flex-wrap: wrap; +`; + +const Emoji = styled(Column)` + padding: 8px; + border-radius: var(--border-radius); + background: var(--secondary-background); +`; + +const Preview = styled.img` + width: 72px; + height: 72px; + object-fit: contain; + border-radius: var(--border-radius); +`; + +const UserInfo = styled(Row)` + font-size: 12px; + + svg { + width: 14px; + height: 14px; + } +`; + +export const Emojis = observer(({ server }: Props) => { + const emoji = [...server.client.emojis.values()].filter( + (x) => x.parent.id === server._id, + ); + + return ( + + {server.havePermission("ManageCustomisation") && ( + + )} +

+ + {" – "} + {emoji.length} +

+ + {emoji.map((emoji) => ( + + + + + {`:${emoji.name}:`} + + + + + {server.havePermission("ManageCustomisation") && ( + + )} + + ))} + +
+ ); +}); diff --git a/src/pages/settings/server/Invites.tsx b/src/pages/settings/server/Invites.tsx index f4f64d1f..d4256427 100644 --- a/src/pages/settings/server/Invites.tsx +++ b/src/pages/settings/server/Invites.tsx @@ -7,12 +7,11 @@ import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useEffect, useState } from "preact/hooks"; -import { getChannelName } from "../../../context/revoltjs/util"; +import { IconButton, Preloader } from "@revoltchat/ui"; import UserIcon from "../../../components/common/user/UserIcon"; import { Username } from "../../../components/common/user/UserShort"; -import IconButton from "../../../components/ui/IconButton"; -import Preloader from "../../../components/ui/Preloader"; +import { ChannelName } from "../../../controllers/client/jsx/ChannelName"; interface InnerProps { invite: API.Invite; @@ -33,13 +32,14 @@ const Inner = observer(({ invite, server, removeSelf }: InnerProps) => { {" "} - {channel ? getChannelName(channel, true) : "#??"} + + + { setDelete(true); server.client.deleteInvite(invite._id).then(removeSelf); - }} - disabled={deleting}> + }}>
diff --git a/src/pages/settings/server/Members.tsx b/src/pages/settings/server/Members.tsx index 0bf5e96a..8b155b4c 100644 --- a/src/pages/settings/server/Members.tsx +++ b/src/pages/settings/server/Members.tsx @@ -9,14 +9,17 @@ import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useEffect, useMemo, useState } from "preact/hooks"; +import { + Button, + Category, + Checkbox, + IconButton, + InputBox, + Preloader, +} from "@revoltchat/ui"; + import UserIcon from "../../../components/common/user/UserIcon"; import { Username } from "../../../components/common/user/UserShort"; -import Button from "../../../components/ui/Button"; -import Checkbox from "../../../components/ui/Checkbox"; -import IconButton from "../../../components/ui/IconButton"; -import InputBox from "../../../components/ui/InputBox"; -import Overline from "../../../components/ui/Overline"; -import { Preloader } from "@revoltchat/ui"; interface InnerProps { member: Member; @@ -48,13 +51,21 @@ const Inner = observer(({ member }: InnerProps) => {
{open && (
- Roles + Roles {Object.keys(server_roles).map((key) => { const role = server_roles[key]; return ( + {role.name} + + } onChange={(v) => { if (v) { setRoles([...roles, key]); @@ -63,18 +74,12 @@ const Inner = observer(({ member }: InnerProps) => { roles.filter((x) => x !== key), ); } - }}> - - {role.name} - - + }} + /> ); })}

diff --git a/src/pages/settings/server/Roles.tsx b/src/pages/settings/server/Roles.tsx index cb2680e8..de43c626 100644 --- a/src/pages/settings/server/Roles.tsx +++ b/src/pages/settings/server/Roles.tsx @@ -5,16 +5,20 @@ import { Server } from "revolt.js"; import { Text } from "preact-i18n"; import { useMemo, useState } from "preact/hooks"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - -import Checkbox from "../../../components/ui/Checkbox"; -import ColourSwatches from "../../../components/ui/ColourSwatches"; -import InputBox from "../../../components/ui/InputBox"; -import Overline from "../../../components/ui/Overline"; -import { Button, PermissionsLayout, SpaceBetween, H1 } from "@revoltchat/ui"; +import { + Button, + PermissionsLayout, + SpaceBetween, + H1, + Checkbox, + ColourSwatches, + InputBox, + Category, +} from "@revoltchat/ui"; import { PermissionList } from "../../../components/settings/roles/PermissionList"; import { RoleOrDefault } from "../../../components/settings/roles/RoleSelection"; +import { modalController } from "../../../controllers/modals/ModalController"; interface Props { server: Server; @@ -49,18 +53,14 @@ export const Roles = observer(({ server }: Props) => { // Consolidate all permissions that we can change right now. const currentRoles = useRoles(server); - // Pull in modal context. - const { openScreen } = useIntermediate(); - return ( - openScreen({ - id: "special_input", + modalController.push({ type: "create_role", - server: server as any, + server, callback, }) } @@ -131,9 +131,9 @@ export const Roles = observer(({ server }: Props) => { {selected !== "default" && ( <>
- + - +

{ name: e.currentTarget.value, }) } - contrast + palette="secondary" />

- + - +

{

- + - +

setValue({ ...value, hoist }) } + title={ + + } description={ - }> - - + } + />

- + - +

{ ), }) } - contrast + palette="secondary" />

diff --git a/src/preact.d.ts b/src/preact.d.ts deleted file mode 100644 index 6f87c025..00000000 --- a/src/preact.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -import JSX = preact.JSX; diff --git a/src/sw.ts b/src/sw.ts index 5daf6258..1daec57c 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -11,7 +11,7 @@ cleanupOutdatedCaches(); // Generate list using scripts/locale.js // prettier-ignore -var locale_keys = ["af","am","ar-dz","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh","ang","ar","az","be","bg","bn","bottom","br","ca","ca@valencia","cs","cy","da","de","de_CH","el","en","en_US","enchantment","enm","eo","es","et","eu","fa","fi","fil","fr","frm","ga","got","he","hi","hr","hu","id","it","ja","ko","la","lb","leet","li","lt","lv","mk","ml","ms","mt","nb_NO","nl","owo","peo","piglatin","pl","pr","pt_BR","pt_PT","ro","ro_MD","ru","si","sk","sl","sq","sr","sv","ta","te","th","tlh-qaak","tokipona","tr","uk","vi","zh_Hans","zh_Hant"]; +const locale_keys = ["af","am","ar-dz","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv-fi","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh","ang","ar","az","be","bg","bn","bottom","br","ca","ca@valencia","ckb","contributors","cs","cy","da","de","de_CH","el","en","en_US","enchantment","enm","eo","es","et","eu","fa","fi","fil","fr","frm","ga","got","he","hi","hr","hu","id","it","ja","kmr","ko","la","lb","leet","li","lt","lv","mk","ml","ms","mt","nb_NO","nl","owo","peo","piglatin","pl","pr","pt_BR","pt_PT","ro","ro_MD","ru","si","sk","sl","sq","sr","sv","ta","te","th","tlh-qaak","tokipona","tr","uk","vec","vi","zh_Hans","zh_Hant"]; precacheAndRoute( self.__WB_MANIFEST.filter((entry) => { @@ -29,7 +29,7 @@ precacheAndRoute( } for (const key of locale_keys) { - if (fn.startsWith(key + ".")) { + if (fn.startsWith(`${key }.`)) { return false; } } diff --git a/src/types/Preact.ts b/src/types/Preact.ts deleted file mode 100644 index dbbea16e..00000000 --- a/src/types/Preact.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { VNode } from "preact"; - -export type Child = VNode | string | number | boolean | undefined | null; -export type Children = Child | Child[] | Children[]; diff --git a/src/env.d.ts b/src/types/env.d.ts similarity index 100% rename from src/env.d.ts rename to src/types/env.d.ts diff --git a/src/globals.d.ts b/src/types/native.d.ts similarity index 100% rename from src/globals.d.ts rename to src/types/native.d.ts diff --git a/src/types/preact.d.ts b/src/types/preact.d.ts new file mode 100644 index 00000000..bf76e9d5 --- /dev/null +++ b/src/types/preact.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +import JSX = preact.JSX; + +declare type Child = + | JSX.Element + | preact.VNode + | string + | number + | boolean + | undefined + | null; + +declare type Children = Child | Child[] | Children[]; diff --git a/src/types/revolt-api.d.ts b/src/types/revolt-api.d.ts new file mode 100644 index 00000000..577b1b52 --- /dev/null +++ b/src/types/revolt-api.d.ts @@ -0,0 +1,9 @@ +// TODO: re-export from revolt-api in some way +declare type Session = { + _id?: string; + token: string; + name: string; + user_id: string; +}; + +declare type SessionPrivate = Session; diff --git a/src/vite-env.d.ts b/src/types/vite-env.d.ts similarity index 100% rename from src/vite-env.d.ts rename to src/types/vite-env.d.ts diff --git a/src/updateWorker.ts b/src/updateWorker.ts new file mode 100644 index 00000000..5043ef90 --- /dev/null +++ b/src/updateWorker.ts @@ -0,0 +1,118 @@ +import isEqual from "lodash.isequal"; +import semver from "semver"; +import { ulid } from "ulid"; +import { registerSW } from "virtual:pwa-register"; + +import { useEffect, useState } from "preact/hooks"; + +import { internalEmit, internalSubscribe } from "./lib/eventEmitter"; + +import { modalController } from "./controllers/modals/ModalController"; +import { APP_VERSION } from "./version"; + +const INTERVAL_HOUR = 36e5; + +let forceUpdate = false; +let registration: ServiceWorkerRegistration | undefined; + +export const updateSW = registerSW({ + onNeedRefresh() { + if (forceUpdate) { + updateSW(true); + } else { + internalEmit("PWA", "update"); + } + }, + onOfflineReady() { + console.info("Ready to work offline."); + // show a ready to work offline to user + }, + onRegistered(r) { + registration = r; + + // Check for updates every hour + setInterval(() => r!.update(), INTERVAL_HOUR); + }, +}); + +let currentPollRate: number; +let scheduledTask: number; + +/** + * Schedule version checker + * @param poll_rate Set poll rate in milliseconds + */ +function schedule(poll_rate = INTERVAL_HOUR) { + if (poll_rate !== currentPollRate) { + currentPollRate = poll_rate; + clearInterval(scheduledTask); + scheduledTask = setInterval( + checkVersion, + poll_rate, + ) as unknown as number; + } +} + +let currentAlert: SystemAlert | undefined; +type SystemAlert = { + text: string; + dismissable?: boolean; + actions?: { + text: string; + type: "internal" | "external"; + href: string; + }[]; +}; + +/** + * Get the current system alert + */ +export function useSystemAlert() { + const [alert, setAlert] = useState(currentAlert); + useEffect(() => internalSubscribe("System", "alert", setAlert as any), []); + return alert; +} + +/** + * Check whether the client is out of date + */ +async function checkVersion() { + const { version, poll_rate, alert } = (await fetch( + "https://api.revolt.chat/release", + ).then((res) => res.json())) as { + version: string; + poll_rate?: number; + alert?: SystemAlert; + }; + + // Re-schedule if necessary + schedule(poll_rate); + + // Apply any active alerts + if (!isEqual(alert, currentAlert)) { + currentAlert = alert; + internalEmit("System", "alert", alert); + } + + // Check if we need to update + if (version !== "0.5.3-7" && !semver.satisfies(APP_VERSION, version)) { + // Let the worker know we should immediately refresh + forceUpdate = true; + + // Prompt service worker to update + registration?.update(); + + // Push information that the client is out of date + modalController.push({ + key: ulid(), + type: "out_of_date", + version, + }); + } +} + +if (import.meta.env.VITE_API_URL === "https://api.revolt.chat") { + // Check for critical updates hourly + schedule(); + checkVersion(); +} diff --git a/tsconfig.json b/tsconfig.json index 1df4e75d..c4a59366 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,5 @@ "types": ["vite-plugin-pwa/client"], "experimentalDecorators": true }, - "include": ["src", "ui/ui.tsx", "external/lang/Languages.ts"] + "include": ["src", "external/lang/Languages.ts"] } diff --git a/ui/index.html b/ui/index.html deleted file mode 100644 index 51074353..00000000 --- a/ui/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Revolt UI - - -
- - - diff --git a/ui/ui.tsx b/ui/ui.tsx deleted file mode 100644 index e6db65af..00000000 --- a/ui/ui.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import styled from "styled-components/macro"; - -import "../src/styles/index.scss"; -import { render } from "preact"; -import { useState } from "preact/hooks"; - -import Theme from "../src/context/Theme"; - -import Banner from "../src/components/ui/Banner"; -import Button from "../src/components/ui/Button"; -import Checkbox from "../src/components/ui/Checkbox"; -import ColourSwatches from "../src/components/ui/ColourSwatches"; -import ComboBox from "../src/components/ui/ComboBox"; -import InputBox from "../src/components/ui/InputBox"; -import Overline from "../src/components/ui/Overline"; -import Radio from "../src/components/ui/Radio"; -import Tip from "../src/components/ui/Tip"; - -export const UIDemo = styled.div` - gap: 12px; - padding: 12px; - display: flex; - flex-direction: column; - align-items: flex-start; -`; - -export function UI() { - let [checked, setChecked] = useState(false); - let [colour, setColour] = useState("#FD6671"); - let [selected, setSelected] = useState<"a" | "b" | "c">("a"); - - return ( - <> - - - - - I am a banner! - - Do you want thing?? - - - - - - - - - - - - setColour(v)} /> - I am a tip! I provide valuable information. - setSelected("a")}> - First option - - setSelected("b")}> - Second option - - setSelected("c")}> - Last option - - Normal overline - Subtle overline - Error overline - Normal overline - - Subtle overline - - - ); -} - -render( - <> - - - - - , - document.getElementById("app")!, -); diff --git a/vercel.json b/vercel.json deleted file mode 100644 index 15cd94df..00000000 --- a/vercel.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] -} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 4558623b..45f8d543 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -39,7 +39,7 @@ function getGitBranch() { } function getVersion() { - return readFileSync("VERSION").toString(); + return JSON.parse(readFileSync("package.json").toString()).version; } export default defineConfig({ @@ -119,7 +119,6 @@ export default defineConfig({ rollupOptions: { input: { main: resolve(__dirname, "index.html"), - ui: resolve(__dirname, "ui/index.html"), }, }, }, diff --git a/yarn.lock b/yarn.lock index 87e1b60b..c433932b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,16 @@ __metadata: version: 6 cacheKey: 8 +"@ampproject/remapping@npm:^2.1.0": + version: 2.2.0 + resolution: "@ampproject/remapping@npm:2.2.0" + dependencies: + "@jridgewell/gen-mapping": ^0.1.0 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: d74d170d06468913921d72430259424b7e4c826b5a7d39ff839a29d547efb97dc577caa8ba3fb5cf023624e9af9d09651afc3d4112a45e2050328abc9b3a2292 + languageName: node + linkType: hard + "@apideck/better-ajv-errors@npm:^0.3.1": version: 0.3.2 resolution: "@apideck/better-ajv-errors@npm:0.3.2" @@ -52,10 +62,10 @@ __metadata: languageName: node linkType: hard -"@babel/compat-data@npm:^7.16.4": - version: 7.16.8 - resolution: "@babel/compat-data@npm:7.16.8" - checksum: 10da2dac5ea9589c251412b00920889910e476c1ab24cd7095577635bc3a27c785151c89db4e26285fd39f509510ec29ab9d7e721f4fc16e4aec221cacde784b +"@babel/compat-data@npm:^7.17.10": + version: 7.17.10 + resolution: "@babel/compat-data@npm:7.17.10" + checksum: e85051087cd4690de5061909a2dd2d7f8b6434a3c2e30be6c119758db2027ae1845bcd75a81127423dd568b706ac6994a1a3d7d701069a23bf5cfe900728290b languageName: node linkType: hard @@ -107,25 +117,25 @@ __metadata: linkType: hard "@babel/core@npm:^7.13.10": - version: 7.16.7 - resolution: "@babel/core@npm:7.16.7" + version: 7.18.2 + resolution: "@babel/core@npm:7.18.2" dependencies: + "@ampproject/remapping": ^2.1.0 "@babel/code-frame": ^7.16.7 - "@babel/generator": ^7.16.7 - "@babel/helper-compilation-targets": ^7.16.7 - "@babel/helper-module-transforms": ^7.16.7 - "@babel/helpers": ^7.16.7 - "@babel/parser": ^7.16.7 + "@babel/generator": ^7.18.2 + "@babel/helper-compilation-targets": ^7.18.2 + "@babel/helper-module-transforms": ^7.18.0 + "@babel/helpers": ^7.18.2 + "@babel/parser": ^7.18.0 "@babel/template": ^7.16.7 - "@babel/traverse": ^7.16.7 - "@babel/types": ^7.16.7 + "@babel/traverse": ^7.18.2 + "@babel/types": ^7.18.2 convert-source-map: ^1.7.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 - json5: ^2.1.2 + json5: ^2.2.1 semver: ^6.3.0 - source-map: ^0.5.0 - checksum: 3206e077e76db189726c4da19a5296eae11c6c1f5abea7013e74f18708bb91616914717ff8d8ca466cc0ba9d2d2147e9a84c3c357b9ad4cba601da14107838ed + checksum: 14a4142c12e004cd2477b7610408d5788ee5dd821ee9e4de204cbb72d9c399d858d9deabc3d49914d5d7c2927548160c19bdc7524b1a9f6acc1ec96a8d9848dd languageName: node linkType: hard @@ -151,7 +161,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.16.7, @babel/generator@npm:^7.16.8": +"@babel/generator@npm:^7.16.8": version: 7.16.8 resolution: "@babel/generator@npm:7.16.8" dependencies: @@ -162,6 +172,17 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.18.2": + version: 7.18.2 + resolution: "@babel/generator@npm:7.18.2" + dependencies: + "@babel/types": ^7.18.2 + "@jridgewell/gen-mapping": ^0.3.0 + jsesc: ^2.5.1 + checksum: d0661e95532ddd97566d41fec26355a7b28d1cbc4df95fe80cc084c413342935911b48db20910708db39714844ddd614f61c2ec4cca3fb10181418bdcaa2e7a3 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.0.0, @babel/helper-annotate-as-pure@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-annotate-as-pure@npm:7.14.5" @@ -204,17 +225,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.16.7": - version: 7.16.7 - resolution: "@babel/helper-compilation-targets@npm:7.16.7" +"@babel/helper-compilation-targets@npm:^7.18.2": + version: 7.18.2 + resolution: "@babel/helper-compilation-targets@npm:7.18.2" dependencies: - "@babel/compat-data": ^7.16.4 + "@babel/compat-data": ^7.17.10 "@babel/helper-validator-option": ^7.16.7 - browserslist: ^4.17.5 + browserslist: ^4.20.2 semver: ^6.3.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: 7238aaee78c011a42fb5ca92e5eff098752f7b314c2111d7bb9cdd58792fcab1b9c819b59f6a0851dc210dc09dc06b30d130a23982753e70eb3111bc65204842 + checksum: 4f02e79f20c0b3f8db5049ba8c35027c41ccb3fc7884835d04e49886538e0f55702959db1bb75213c94a5708fec2dc81a443047559a4f184abb884c72c0059b4 languageName: node linkType: hard @@ -290,6 +311,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-environment-visitor@npm:^7.18.2": + version: 7.18.2 + resolution: "@babel/helper-environment-visitor@npm:7.18.2" + checksum: 1a9c8726fad454a082d077952a90f17188e92eabb3de236cb4782c49b39e3f69c327e272b965e9a20ff8abf37d30d03ffa6fd7974625a6c23946f70f7527f5e9 + languageName: node + linkType: hard + "@babel/helper-explode-assignable-expression@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-explode-assignable-expression@npm:7.14.5" @@ -428,19 +456,19 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.16.7": - version: 7.16.7 - resolution: "@babel/helper-module-transforms@npm:7.16.7" +"@babel/helper-module-transforms@npm:^7.18.0": + version: 7.18.0 + resolution: "@babel/helper-module-transforms@npm:7.18.0" dependencies: "@babel/helper-environment-visitor": ^7.16.7 "@babel/helper-module-imports": ^7.16.7 - "@babel/helper-simple-access": ^7.16.7 + "@babel/helper-simple-access": ^7.17.7 "@babel/helper-split-export-declaration": ^7.16.7 "@babel/helper-validator-identifier": ^7.16.7 "@babel/template": ^7.16.7 - "@babel/traverse": ^7.16.7 - "@babel/types": ^7.16.7 - checksum: 6e930ce776c979f299cdbeaf80187f4ab086d75287b96ecc1c6896d392fcb561065f0d6219fc06fa79b4ceb4bbdc1a9847da8099aba9b077d0a9e583500fb673 + "@babel/traverse": ^7.18.0 + "@babel/types": ^7.18.0 + checksum: 824c3967c08d75bb36adc18c31dcafebcd495b75b723e2e17c6185e88daf5c6db62a6a75d9f791b5f38618a349e7cb32503e715a1b9a4e8bad4d0f43e3e6b523 languageName: node linkType: hard @@ -476,6 +504,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.17.12": + version: 7.17.12 + resolution: "@babel/helper-plugin-utils@npm:7.17.12" + checksum: 4813cf0ddb0f143de032cb88d4207024a2334951db330f8216d6fa253ea320c02c9b2667429ef1a34b5e95d4cfbd085f6cb72d418999751c31d0baf2422cc61d + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-remap-async-to-generator@npm:7.14.5" @@ -521,12 +556,12 @@ __metadata: languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.16.7": - version: 7.16.7 - resolution: "@babel/helper-simple-access@npm:7.16.7" +"@babel/helper-simple-access@npm:^7.17.7": + version: 7.18.2 + resolution: "@babel/helper-simple-access@npm:7.18.2" dependencies: - "@babel/types": ^7.16.7 - checksum: 8d22c46c5ec2ead0686c4d5a3d1d12b5190c59be676bfe0d9d89df62b437b51d1a3df2ccfb8a77dded2e585176ebf12986accb6d45a18cff229eef3b10344f4b + "@babel/types": ^7.18.2 + checksum: c0862b56db7e120754d89273a039b128c27517389f6a4425ff24e49779791e8fe10061579171fb986be81fa076778acb847c709f6f5e396278d9c5e01360c375 languageName: node linkType: hard @@ -608,14 +643,14 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.16.7": - version: 7.16.7 - resolution: "@babel/helpers@npm:7.16.7" +"@babel/helpers@npm:^7.18.2": + version: 7.18.2 + resolution: "@babel/helpers@npm:7.18.2" dependencies: "@babel/template": ^7.16.7 - "@babel/traverse": ^7.16.7 - "@babel/types": ^7.16.7 - checksum: 75504c76b66a29b91f954fcc0867dfe275a4cfba5b44df6d64405df74ea72f967fccfa63d62c31c423c5502d113290000c581e0e4858a214f0303d7ecf55c29f + "@babel/traverse": ^7.18.2 + "@babel/types": ^7.18.2 + checksum: 94620242f23f6d5f9b83a02b1aa1632ffb05b0815e1bb53d3b46d64aa8e771066bba1db8bd267d9091fb00134cfaeda6a8d69d1d4cc2c89658631adfa077ae70 languageName: node linkType: hard @@ -650,12 +685,12 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.16.7, @babel/parser@npm:^7.16.8": - version: 7.16.8 - resolution: "@babel/parser@npm:7.16.8" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.18.0": + version: 7.18.3 + resolution: "@babel/parser@npm:7.18.3" bin: parser: ./bin/babel-parser.js - checksum: f6bc2eb1f298fcb81db34c2d343fd05d8c59dbc5419a88c1cb4d298c7a3863e4d54f5a4f38a40e1aa979e4ce355816348730b471c1d787d424ed52b270fc7be0 + checksum: 6894b3266f84b6c6b52bf09e7f61526efc35d8afa72ff0ad9aecb27a4b6de02d1ebc7f61fc3ae7c0fd8ecb5ac17083d1f27c1b3176e5eac41131d7160a9a7d88 languageName: node linkType: hard @@ -668,6 +703,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.16.7, @babel/parser@npm:^7.16.8": + version: 7.16.8 + resolution: "@babel/parser@npm:7.16.8" + bin: + parser: ./bin/babel-parser.js + checksum: f6bc2eb1f298fcb81db34c2d343fd05d8c59dbc5419a88c1cb4d298c7a3863e4d54f5a4f38a40e1aa979e4ce355816348730b471c1d787d424ed52b270fc7be0 + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.14.5": version: 7.14.5 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.14.5" @@ -963,13 +1007,13 @@ __metadata: linkType: hard "@babel/plugin-syntax-jsx@npm:^7.12.13": - version: 7.16.7 - resolution: "@babel/plugin-syntax-jsx@npm:7.16.7" + version: 7.17.12 + resolution: "@babel/plugin-syntax-jsx@npm:7.17.12" dependencies: - "@babel/helper-plugin-utils": ^7.16.7 + "@babel/helper-plugin-utils": ^7.17.12 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: cd9b0e53c50e8ddb0afaf0f42e0b221a94e4f59aee32a591364266a31195c48cac5fef288d02c1c935686bda982d2e0f1ed61cceb995fc9f6fb09ef5ebecdd2b + checksum: 6acd0bbca8c3e0100ad61f3b7d0b0111cd241a0710b120b298c4aa0e07be02eccbcca61ede1e7678ade1783a0979f20305b62263df6767fa3fbf658670d82af5 languageName: node linkType: hard @@ -1062,13 +1106,13 @@ __metadata: linkType: hard "@babel/plugin-syntax-typescript@npm:^7.12.13": - version: 7.16.7 - resolution: "@babel/plugin-syntax-typescript@npm:7.16.7" + version: 7.17.12 + resolution: "@babel/plugin-syntax-typescript@npm:7.17.12" dependencies: - "@babel/helper-plugin-utils": ^7.16.7 + "@babel/helper-plugin-utils": ^7.17.12 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 661e636060609ede9a402e22603b01784c21fabb0a637e65f561c8159351fe0130bbc11fdefe31902107885e3332fc34d95eb652ac61d3f61f2d61f5da20609e + checksum: 50ab09f1953a2b0586cff9e29bf7cea3d886b48c1361a861687c2aef46356c6d73778c3341b0c051dc82a34417f19e9d759ae918353c5a98d25e85f2f6d24181 languageName: node linkType: hard @@ -1546,7 +1590,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.5, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.5, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.8.4": version: 7.15.3 resolution: "@babel/runtime@npm:7.15.3" dependencies: @@ -1555,12 +1599,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.12.5": - version: 7.16.7 - resolution: "@babel/runtime@npm:7.16.7" +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.9.2": + version: 7.18.3 + resolution: "@babel/runtime@npm:7.18.3" dependencies: regenerator-runtime: ^0.13.4 - checksum: 47912f0aaacd1cab2e2552aaf3e6eaffbcaf2d5ac9b07a89a12ac0d42029cb92c070b0d16f825e4277c4a34677c54d8ffe85e1f7c6feb57de58f700eec67ce2f + checksum: db8526226aa02cfa35a5a7ac1a34b5f303c62a1f000c7db48cb06c6290e616483e5036ab3c4e7a84d0f3be6d4e2148d5fe5cec9564bf955f505c3e764b83d7f1 languageName: node linkType: hard @@ -1654,6 +1698,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.18.0, @babel/traverse@npm:^7.18.2": + version: 7.18.2 + resolution: "@babel/traverse@npm:7.18.2" + dependencies: + "@babel/code-frame": ^7.16.7 + "@babel/generator": ^7.18.2 + "@babel/helper-environment-visitor": ^7.18.2 + "@babel/helper-function-name": ^7.17.9 + "@babel/helper-hoist-variables": ^7.16.7 + "@babel/helper-split-export-declaration": ^7.16.7 + "@babel/parser": ^7.18.0 + "@babel/types": ^7.18.2 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: e21c2d550bf610406cf21ef6fbec525cb1d80b9d6d71af67552478a24ee371203cb4025b23b110ae7288a62a874ad5898daad19ad23daa95dfc8ab47a47a092f + languageName: node + linkType: hard + "@babel/types@npm:7.13.0": version: 7.13.0 resolution: "@babel/types@npm:7.13.0" @@ -1665,13 +1727,13 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.7, @babel/types@npm:^7.16.8, @babel/types@npm:^7.3.0": - version: 7.16.8 - resolution: "@babel/types@npm:7.16.8" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.0, @babel/types@npm:^7.18.2, @babel/types@npm:^7.3.0": + version: 7.18.2 + resolution: "@babel/types@npm:7.18.2" dependencies: "@babel/helper-validator-identifier": ^7.16.7 to-fast-properties: ^2.0.0 - checksum: 4f6a187b2924df70e21d6e6c0822f91b1b936fe060bc92bb477b93bd8a712c88fe41a73f85c0ec53b033353374fe33e773b04ffc340ad36afd8f647dd05c4ee1 + checksum: 3750bcb9ef6f36ecf0c1477cf6010cd23f2db5cb93f6771ba84c07c08aa005934532bc81e9067192f85214c43e16731e0e3c244773071879967fd1cd22ba2144 languageName: node linkType: hard @@ -1685,6 +1747,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.16.7, @babel/types@npm:^7.16.8": + version: 7.16.8 + resolution: "@babel/types@npm:7.16.8" + dependencies: + "@babel/helper-validator-identifier": ^7.16.7 + to-fast-properties: ^2.0.0 + checksum: 4f6a187b2924df70e21d6e6c0822f91b1b936fe060bc92bb477b93bd8a712c88fe41a73f85c0ec53b033353374fe33e773b04ffc340ad36afd8f647dd05c4ee1 + languageName: node + linkType: hard + "@babel/types@npm:^7.17.0, @babel/types@npm:^7.8.3": version: 7.17.0 resolution: "@babel/types@npm:7.17.0" @@ -1725,6 +1797,17 @@ __metadata: languageName: node linkType: hard +"@es-joy/jsdoccomment@npm:~0.31.0": + version: 0.31.0 + resolution: "@es-joy/jsdoccomment@npm:0.31.0" + dependencies: + comment-parser: 1.3.1 + esquery: ^1.4.0 + jsdoc-type-pratt-parser: ~3.1.0 + checksum: 1691ff501559f45593e5f080d2c08dea4fadba5f48e526b9ff2943c050fbb40408f5e83968542e5b6bf47219c7573796d00bfe80dacfd1ba8187904cc475cefb + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^0.4.3": version: 0.4.3 resolution: "@eslint/eslintrc@npm:0.4.3" @@ -1749,10 +1832,10 @@ __metadata: languageName: node linkType: hard -"@fontsource/bitter@npm:^4.5.0": - version: 4.5.0 - resolution: "@fontsource/bitter@npm:4.5.0" - checksum: f87d9cb04519586adeb3c9e7f069efecd88448c352a5b8409c6aa5046309b0424c41bcb70baaefee3dd731a5c8dafc296c430b9557df11dfde3306ce102f7c75 +"@fontsource/bitter@npm:^4.5.7": + version: 4.5.7 + resolution: "@fontsource/bitter@npm:4.5.7" + checksum: b60995e5411a04d52bc69b45221dc81828e3b13515abb2e4d338ab9f24618fbfebce35aaff9ad48a04691b7b11167c0cdfae280b2c0c0638e6425e110532ff5f languageName: node linkType: hard @@ -1933,9 +2016,9 @@ __metadata: languageName: node linkType: hard -"@insertish/oapi@npm:0.1.15": - version: 0.1.15 - resolution: "@insertish/oapi@npm:0.1.15" +"@insertish/oapi@npm:0.1.16": + version: 0.1.16 + resolution: "@insertish/oapi@npm:0.1.16" dependencies: axios: ^0.26.1 openapi-typescript: ^5.2.0 @@ -1947,7 +2030,7 @@ __metadata: optional: true bin: oapilib: cli.js - checksum: e4b34382f8f64eb6f5e6f9e3df6e607341031c4d4571169d7d7ad75e76f5dabc766de467f9f6a1cdd35818ce39724daa4a5ce487ccb5d069379a6dfab0faf8b6 + checksum: 746e447fd41c6a3925b36af1747c8fe9591e5d93bf119a6a8d22ff76b779f325ec9c0f13dd77ff1957aa54c365c0c8ac9c5efab74c63d4897efe20c8c3270032 languageName: node linkType: hard @@ -1967,6 +2050,58 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.1.0": + version: 0.1.1 + resolution: "@jridgewell/gen-mapping@npm:0.1.1" + dependencies: + "@jridgewell/set-array": ^1.0.0 + "@jridgewell/sourcemap-codec": ^1.4.10 + checksum: 3bcc21fe786de6ffbf35c399a174faab05eb23ce6a03e8769569de28abbf4facc2db36a9ddb0150545ae23a8d35a7cf7237b2aa9e9356a7c626fb4698287d5cc + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.0": + version: 0.3.1 + resolution: "@jridgewell/gen-mapping@npm:0.3.1" + dependencies: + "@jridgewell/set-array": ^1.0.0 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: e9e7bb3335dea9e60872089761d4e8e089597360cdb1af90370e9d53b7d67232c1e0a3ab65fbfef4fc785745193fbc56bff9f3a6cab6c6ce3f15e12b4191f86b + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3": + version: 3.0.7 + resolution: "@jridgewell/resolve-uri@npm:3.0.7" + checksum: 94f454f4cef8f0acaad85745fd3ca6cd0d62ef731cf9f952ecb89b8b2ce5e20998cd52be31311cedc5fa5b28b1708a15f3ad9df0fe1447ee4f42959b036c4b5b + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.0.0": + version: 1.1.1 + resolution: "@jridgewell/set-array@npm:1.1.1" + checksum: cc5d91e0381c347e3edee4ca90b3c292df9e6e55f29acbe0dd97de8651b4730e9ab761406fd572effa79972a0edc55647b627f8c72315e276d959508853d9bf2 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.4.13 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.13" + checksum: f14449096f60a5f921262322fef65ce0bbbfb778080b3b20212080bcefdeba621c43a58c27065bd536ecb4cc767b18eb9c45f15b6b98a4970139572b60603a1c + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.13 + resolution: "@jridgewell/trace-mapping@npm:0.3.13" + dependencies: + "@jridgewell/resolve-uri": ^3.0.3 + "@jridgewell/sourcemap-codec": ^1.4.10 + checksum: e38254e830472248ca10a6ed1ae75af5e8514f0680245a5e7b53bc3c030fd8691d4d3115d80595b45d3badead68269769ed47ecbbdd67db1343a11f05700e75a + languageName: node + linkType: hard + "@juggle/resize-observer@npm:^3.3.1": version: 3.3.1 resolution: "@juggle/resize-observer@npm:3.3.1" @@ -1974,6 +2109,15 @@ __metadata: languageName: node linkType: hard +"@mapbox/hast-util-table-cell-style@npm:^0.2.0": + version: 0.2.0 + resolution: "@mapbox/hast-util-table-cell-style@npm:0.2.0" + dependencies: + unist-util-visit: ^1.4.1 + checksum: 4b05edda2be32e3286860bd5b50eddc8fe7d64c88de49511c9188e0e1d7c2fcba3f589a279d87cf8eb42ed5ef9ef0e788d2fcb103613e547cb3143b1bd29c49a + languageName: node + linkType: hard + "@mdn/browser-compat-data@npm:^3.3.14": version: 3.3.14 resolution: "@mdn/browser-compat-data@npm:3.3.14" @@ -2096,18 +2240,23 @@ __metadata: languageName: node linkType: hard -"@revoltchat/ui@npm:1.0.31": - version: 1.0.31 - resolution: "@revoltchat/ui@npm:1.0.31" +"@revoltchat/ui@npm:1.0.76": + version: 1.0.76 + resolution: "@revoltchat/ui@npm:1.0.76" dependencies: "@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0 "@styled-icons/boxicons-solid": ^10.38.0 - mobx: ^6.5.0 - mobx-react-lite: ^3.3.0 + "@tippyjs/react": ^4.2.6 + mobx: ^6.6.0 + mobx-react-lite: ^3.4.0 + prismjs: ^1.28.0 + react-beautiful-dnd: ^13.1.0 + react-device-detect: ^2.2.2 + react-virtuoso: ^2.12.0 peerDependencies: - revolt-api: "*" - checksum: 8f93757d131ae7d784e744b774f6f6b9bea4e09a3fb48762334caf23e4ab08db209649eba1435f9fa82bdf7bfbfb78100fef6d13e7e0f715d0e2c82c52abf74d + revolt.js: "*" + checksum: b4051c759bd2e350eaab0f28c4d27b8df391086d63d58db14edf0c557d19da6d1f95df20327682086db46c6348ab7c7a0e6c696829e16d18fd341e4fbc8cbaf2 languageName: node linkType: hard @@ -2256,15 +2405,15 @@ __metadata: languageName: node linkType: hard -"@tippyjs/react@npm:^4.2.5": - version: 4.2.5 - resolution: "@tippyjs/react@npm:4.2.5" +"@tippyjs/react@npm:4.2.6, @tippyjs/react@npm:^4.2.6": + version: 4.2.6 + resolution: "@tippyjs/react@npm:4.2.6" dependencies: tippy.js: ^6.3.1 peerDependencies: react: ">=16.8" react-dom: ">=16.8" - checksum: 68a6bb8922597df105f601953f14c593a8179328026dc425db0cd5d8521cdd8ad8c6ec7b6d0707708c8ed25e5ad01c488e95a6b3de0b2f404bd71137e2b8fce9 + checksum: 8f0fba591c9dae2e1af1ae632bbc775ba5c9dd4498e50e242be70302b4c27115c6740eec44e885e294b27cb28515777b52af5b34aac9d4bab627d948add938ae languageName: node linkType: hard @@ -2310,15 +2459,15 @@ __metadata: linkType: hard "@types/babel__core@npm:^7.1.12": - version: 7.1.18 - resolution: "@types/babel__core@npm:7.1.18" + version: 7.1.19 + resolution: "@types/babel__core@npm:7.1.19" dependencies: "@babel/parser": ^7.1.0 "@babel/types": ^7.0.0 "@types/babel__generator": "*" "@types/babel__template": "*" "@types/babel__traverse": "*" - checksum: 2e5b5d7c84f347d3789575486e58b0df5c91613abc3d27e716274aba3048518e07e1f068250ba829e2ed58532ccc88da595ce95ba2688e7bbcd7c25a3c6627ed + checksum: 8c9fa87a1c2224cbec251683a58bebb0d74c497118034166aaa0491a4e2627998a6621fc71f8a60ffd27d9c0c52097defedf7637adc6618d0331c15adb302338 languageName: node linkType: hard @@ -2342,15 +2491,15 @@ __metadata: linkType: hard "@types/babel__traverse@npm:*": - version: 7.14.2 - resolution: "@types/babel__traverse@npm:7.14.2" + version: 7.17.1 + resolution: "@types/babel__traverse@npm:7.17.1" dependencies: "@babel/types": ^7.3.0 - checksum: a797ea09c72307569e3ee08aa3900ca744ce3091114084f2dc59b67a45ee7d01df7865252790dbfa787a7915ce892cdc820c9b920f3683292765fc656b08dc63 + checksum: 8992d8c1eaaf1c793e9184b930767883446939d2744c40ea4e9591086e79b631189dc519931ed8864f1e016742a189703c217db59b800aca84870b865009d8b4 languageName: node linkType: hard -"@types/debug@npm:^4.1.6": +"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.6": version: 4.1.7 resolution: "@types/debug@npm:4.1.7" dependencies: @@ -2373,6 +2522,15 @@ __metadata: languageName: node linkType: hard +"@types/hast@npm:*, @types/hast@npm:^2.0.0": + version: 2.3.4 + resolution: "@types/hast@npm:2.3.4" + dependencies: + "@types/unist": "*" + checksum: fff47998f4c11e21a7454b58673f70478740ecdafd95aaf50b70a3daa7da9cdc57315545bf9c039613732c40b7b0e9e49d11d03fe9a4304721cdc3b29a88141e + languageName: node + linkType: hard + "@types/history@npm:*": version: 4.7.9 resolution: "@types/history@npm:4.7.9" @@ -2390,6 +2548,13 @@ __metadata: languageName: node linkType: hard +"@types/is-hotkey@npm:^0.1.1": + version: 0.1.7 + resolution: "@types/is-hotkey@npm:0.1.7" + checksum: bce7c8874b30f346f20cf6cfcf4a10372962924f0e1b1a925a279004faeb276242ebfbfee47bd48df57e1021f2e3078c34e25837e226960c418d5f78f83dacea + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.3, @types/json-schema@npm:^7.0.7": version: 7.0.9 resolution: "@types/json-schema@npm:7.0.9" @@ -2397,10 +2562,10 @@ __metadata: languageName: node linkType: hard -"@types/linkify-it@npm:*": - version: 3.0.2 - resolution: "@types/linkify-it@npm:3.0.2" - checksum: dff8f10fafb885422474e456596f12d518ec4cdd6c33cca7a08e7c86b912d301ed91cf5a7613e148c45a12600dc9ab3d85ad16d5b48dc1aaeda151a68f16b536 +"@types/katex@npm:^0.11.0": + version: 0.11.1 + resolution: "@types/katex@npm:0.11.1" + checksum: 1e51988b4b386a1b6fa8e22826ab4705bf3e6c9fb03461f2c91d28cb31095232bdeff491069ac9bc74bc4c26110be6a11a41e12ca77a2e4169f3afd8cd349355 languageName: node linkType: hard @@ -2436,18 +2601,23 @@ __metadata: languageName: node linkType: hard -"@types/markdown-it@npm:^12.0.2": - version: 12.2.1 - resolution: "@types/markdown-it@npm:12.2.1" - dependencies: - "@types/linkify-it": "*" - "@types/mdurl": "*" - highlight.js: ^10.7.2 - checksum: e3f367b7006e4ade2b8faa7125f06f6e5dd24ff762faedad7c55d1c99dc9905213a1021c7ae019ed1693814ec31fef25be028415d4d3061186bbf2d67646ddcb +"@types/lodash@npm:^4, @types/lodash@npm:^4.14.149": + version: 4.14.182 + resolution: "@types/lodash@npm:4.14.182" + checksum: 7dd137aa9dbabd632408bd37009d984655164fa1ecc3f2b6eb94afe35bf0a5852cbab6183148d883e9c73a958b7fec9a9bcf7c8e45d41195add6a18c34958209 languageName: node linkType: hard -"@types/mdurl@npm:*": +"@types/mdast@npm:*, @types/mdast@npm:^3.0.0": + version: 3.0.10 + resolution: "@types/mdast@npm:3.0.10" + dependencies: + "@types/unist": "*" + checksum: 3f587bfc0a9a2403ecadc220e61031b01734fedaf82e27eb4d5ba039c0eb54db8c85681ccc070ab4df3f7ec711b736a82b990e69caa14c74bf7ac0ccf2ac7313 + languageName: node + linkType: hard + +"@types/mdurl@npm:^1.0.0": version: 1.0.2 resolution: "@types/mdurl@npm:1.0.2" checksum: 79c7e523b377f53cf1f5a240fe23d0c6cae856667692bd21bf1d064eafe5ccc40ae39a2aa0a9a51e8c94d1307228c8f6b121e847124591a9a828c3baf65e86e2 @@ -2468,6 +2638,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>12": + version: 18.0.3 + resolution: "@types/node@npm:18.0.3" + checksum: 5dec59fbbc1186c808b53df1ca717dad034dbd6a901c75f5b052c845618b531b05f27217122c6254db99529a68618e4cfc534ae3dbf4e88754e9e572df80defa + languageName: node + linkType: hard + "@types/node@npm:^15.12.4": version: 15.14.9 resolution: "@types/node@npm:15.14.9" @@ -2482,6 +2659,13 @@ __metadata: languageName: node linkType: hard +"@types/parse5@npm:^6.0.0": + version: 6.0.3 + resolution: "@types/parse5@npm:6.0.3" + checksum: ddb59ee4144af5dfcc508a8dcf32f37879d11e12559561e65788756b95b33e6f03ea027d88e1f5408f9b7bfb656bf630ace31a2169edf44151daaf8dd58df1b7 + languageName: node + linkType: hard + "@types/preact-i18n@npm:^2.3.0": version: 2.3.1 resolution: "@types/preact-i18n@npm:2.3.1" @@ -2498,6 +2682,13 @@ __metadata: languageName: node linkType: hard +"@types/prismjs@npm:^1.16.6": + version: 1.26.0 + resolution: "@types/prismjs@npm:1.26.0" + checksum: cd5e7a6214c1f4213ec512a5fcf6d8fe37a56b813fc57ac95b5ff5ee074742bfdbd2f2730d9fd985205bf4586728e09baa97023f739e5aa1c9735a7c1ecbd11a + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.4 resolution: "@types/prop-types@npm:15.7.4" @@ -2505,7 +2696,7 @@ __metadata: languageName: node linkType: hard -"@types/react-beautiful-dnd@npm:^13.1.2": +"@types/react-beautiful-dnd@npm:^13": version: 13.1.2 resolution: "@types/react-beautiful-dnd@npm:13.1.2" dependencies: @@ -2523,15 +2714,15 @@ __metadata: languageName: node linkType: hard -"@types/react-redux@npm:^7.1.16": - version: 7.1.18 - resolution: "@types/react-redux@npm:7.1.18" +"@types/react-redux@npm:^7.1.20": + version: 7.1.24 + resolution: "@types/react-redux@npm:7.1.24" dependencies: "@types/hoist-non-react-statics": ^3.3.0 "@types/react": "*" hoist-non-react-statics: ^3.3.0 redux: ^4.0.0 - checksum: 8aa24c15df711e2a20f903843f42491316094c3a49a90dcae86dcafa8fdb2318fdfaa983e23d67840986f11131b9b8856a5d6971288d68fa8aa592adc348a942 + checksum: 6582246581331ac7fbbd44aa1f1c136c8a9c8febbcf462432ac81302263308c21e1a2e7868beb7f73bbcb52a8e67935d133cb37f5bdcb6564eaff3a811805101 languageName: node linkType: hard @@ -2592,6 +2783,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7": + version: 7.3.9 + resolution: "@types/semver@npm:7.3.9" + checksum: 60bfcfdfa7f937be2c6f4b37ddb6714fb0f27b05fe4cbdfdd596a97d35ed95d13ee410efdd88e72a66449d0384220bf20055ab7d6b5df10de4990fbd20e5cbe0 + languageName: node + linkType: hard + "@types/styled-components@npm:^5.1.10": version: 5.1.13 resolution: "@types/styled-components@npm:5.1.13" @@ -2617,6 +2815,13 @@ __metadata: languageName: node linkType: hard +"@types/unist@npm:*, @types/unist@npm:^2.0.0": + version: 2.0.6 + resolution: "@types/unist@npm:2.0.6" + checksum: 25cb860ff10dde48b54622d58b23e66214211a61c84c0f15f88d38b61aa1b53d4d46e42b557924a93178c501c166aa37e28d7f6d994aba13d24685326272d5db + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^4.27.0": version: 4.29.3 resolution: "@typescript-eslint/eslint-plugin@npm:4.29.3" @@ -2748,21 +2953,21 @@ __metadata: languageName: node linkType: hard -"@virtuoso.dev/react-urx@npm:^0.2.5": - version: 0.2.6 - resolution: "@virtuoso.dev/react-urx@npm:0.2.6" +"@virtuoso.dev/react-urx@npm:^0.2.12": + version: 0.2.13 + resolution: "@virtuoso.dev/react-urx@npm:0.2.13" dependencies: - "@virtuoso.dev/urx": ^0.2.6 + "@virtuoso.dev/urx": ^0.2.13 peerDependencies: react: ">=16" - checksum: 877760d0f4e56e4514a1f4f2e0160a99834b06b3c24bab32e569cadd06a3cb18e651bb60824a105d0abf4cc943630c2f68d0f461931b89f9e5f3ffff497f5c2b + checksum: 173e91c21f6a8cd506ad3b72af10656897fe1951124ed9eeb1fd85575534993bea2f97cba3f81c08ae1e88a2613df348e2c80d0ceecb3021f8c8c8fe0e053ee2 languageName: node linkType: hard -"@virtuoso.dev/urx@npm:^0.2.5, @virtuoso.dev/urx@npm:^0.2.6": - version: 0.2.6 - resolution: "@virtuoso.dev/urx@npm:0.2.6" - checksum: d1942a81a828e250030a1a3dbf66545b1539c29c62d519b1bcaa1a45badf4e1baaa9efecf13238ca6c45555673fe5e12f3aba7d1c4fa2d7ab3e0a9a1504cf153 +"@virtuoso.dev/urx@npm:^0.2.12, @virtuoso.dev/urx@npm:^0.2.13": + version: 0.2.13 + resolution: "@virtuoso.dev/urx@npm:0.2.13" + checksum: 682a99cf40ccc429241268dd37495cd1ed4695ae58b5a1169c75df1630d5dc3fd8eb3aaa655f71c37f39ba9c23c0aaf4401b76d8a986986d1a38a422d596a6ba languageName: node linkType: hard @@ -3140,6 +3345,13 @@ __metadata: languageName: node linkType: hard +"bail@npm:^2.0.0": + version: 2.0.2 + resolution: "bail@npm:2.0.2" + checksum: aab4e8ccdc8d762bf3fdfce8e706601695620c0c2eda256dd85088dc0be3cfd7ff126f6e99c2bee1f24f5d418414aacf09d7f9702f16d6963df2fa488cda8824 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -3154,6 +3366,13 @@ __metadata: languageName: node linkType: hard +"boolbase@npm:^1.0.0": + version: 1.0.0 + resolution: "boolbase@npm:1.0.0" + checksum: 3e25c80ef626c3a3487c73dbfc70ac322ec830666c9ad915d11b701142fab25ec1e63eff2c450c74347acfd2de854ccde865cd79ef4db1683f7c7b046ea43bb0 + languageName: node + linkType: hard + "bowser@npm:^2.11.0": version: 2.11.0 resolution: "bowser@npm:2.11.0" @@ -3204,18 +3423,18 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.17.5": - version: 4.19.1 - resolution: "browserslist@npm:4.19.1" +"browserslist@npm:^4.20.2": + version: 4.20.3 + resolution: "browserslist@npm:4.20.3" dependencies: - caniuse-lite: ^1.0.30001286 - electron-to-chromium: ^1.4.17 + caniuse-lite: ^1.0.30001332 + electron-to-chromium: ^1.4.118 escalade: ^3.1.1 - node-releases: ^2.0.1 + node-releases: ^2.0.3 picocolors: ^1.0.0 bin: browserslist: cli.js - checksum: c0777fd483691638fd6801e16c9d809e1d65f6d2b06db2e806654be51045cbab1452a89841a2c5caea2cbe19d621b4f1d391cffbb24512aa33280039ab345875 + checksum: 1e4b719ac2ca0fe235218a606e8b8ef16b8809e0973b924158c39fbc435a0b0fe43437ea52dd6ef5ad2efcb83fcb07431244e472270177814217f7c563651f7d languageName: node linkType: hard @@ -3283,13 +3502,27 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001251, caniuse-lite@npm:^1.0.30001286": +"caniuse-lite@npm:^1.0.30001251": version: 1.0.30001313 resolution: "caniuse-lite@npm:1.0.30001313" checksum: 49f2dcd1fa493a09a5247dcf3a4da3b9df355131b1fc1fd08b67ae7683c300ed9b9eef6a5424b4ac7e5d1ff0e129d2a0b4adf2a6a5a04ab5c2c0b2c590e935be languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001332": + version: 1.0.30001344 + resolution: "caniuse-lite@npm:1.0.30001344" + checksum: 9dba66f796dc98632dced4c5d487d0fad219e137a27c634eec68520f2e598a613e3371b9207e15a078689a629128eca898793e37fc98841821ab481bddad51b9 + languageName: node + linkType: hard + +"ccount@npm:^2.0.0": + version: 2.0.1 + resolution: "ccount@npm:2.0.1" + checksum: 48193dada54c9e260e0acf57fc16171a225305548f9ad20d5471e0f7a8c026aedd8747091dccb0d900cde7df4e4ddbd235df0d8de4a64c71b12f0d3303eeafd4 + languageName: node + linkType: hard + "chalk@npm:^2.0.0, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -3311,6 +3544,13 @@ __metadata: languageName: node linkType: hard +"character-entities@npm:^2.0.0": + version: 2.0.2 + resolution: "character-entities@npm:2.0.2" + checksum: cf1643814023697f725e47328fcec17923b8f1799102a8a79c1514e894815651794a2bffd84bb1b3a4b124b050154e4529ed6e81f7c8068a734aecf07a6d3def + languageName: node + linkType: hard + "charcodes@npm:^0.2.0": version: 0.2.0 resolution: "charcodes@npm:0.2.0" @@ -3364,7 +3604,7 @@ __metadata: dependencies: "@babel/plugin-proposal-decorators": ^7.17.9 "@fontsource/atkinson-hyperlegible": ^4.4.5 - "@fontsource/bitter": ^4.5.0 + "@fontsource/bitter": ^4.5.7 "@fontsource/comic-neue": ^4.4.5 "@fontsource/fira-code": ^4.4.5 "@fontsource/inter": ^4.4.5 @@ -3386,26 +3626,27 @@ __metadata: "@hcaptcha/react-hcaptcha": ^0.3.6 "@insertish/vite-plugin-babel-macros": ^1.0.5 "@preact/preset-vite": ^2.0.0 - "@revoltchat/ui": 1.0.31 + "@revoltchat/ui": 1.0.76 "@rollup/plugin-replace": ^2.4.2 "@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0 "@styled-icons/boxicons-solid": ^10.38.0 "@styled-icons/simple-icons": ^10.33.0 - "@tippyjs/react": ^4.2.5 + "@tippyjs/react": 4.2.6 "@traptitech/markdown-it-katex": ^3.4.3 "@traptitech/markdown-it-spoiler": ^1.1.6 "@trivago/prettier-plugin-sort-imports": ^2.0.2 + "@types/lodash": ^4 "@types/lodash.defaultsdeep": ^4.6.6 "@types/lodash.isequal": ^4.5.5 - "@types/markdown-it": ^12.0.2 "@types/node": ^15.12.4 "@types/preact-i18n": ^2.3.0 "@types/prismjs": ^1.16.5 - "@types/react-beautiful-dnd": ^13.1.2 + "@types/react-beautiful-dnd": ^13 "@types/react-helmet": ^6.1.1 "@types/react-router-dom": ^5.1.7 "@types/react-scroll": ^1.8.2 + "@types/semver": ^7 "@types/styled-components": ^5.1.10 "@types/twemoji": ^12.1.1 "@typescript-eslint/eslint-plugin": ^4.27.0 @@ -3417,41 +3658,59 @@ __metadata: detect-browser: ^5.2.0 eslint: ^7.28.0 eslint-config-preact: ^1.1.4 + eslint-plugin-jsdoc: ^39.3.2 + eslint-plugin-mobx: ^0.0.8 eventemitter3: ^4.0.7 fs-extra: ^10.0.0 + history: 4 json-stringify-deterministic: ^1.0.2 klaw: ^3.0.0 localforage: ^1.9.0 + lodash: ^4.17.21 lodash.defaultsdeep: ^4.6.1 lodash.isequal: ^4.5.0 long: ^5.2.0 - markdown-it: ^12.0.6 - markdown-it-emoji: ^2.0.0 + mdast-util-to-hast: ^12.1.2 mediasoup-client: "npm:@insertish/mediasoup-client@3.6.36-esnext" - mobx: ^6.3.2 - mobx-react-lite: ^3.3.0 + mobx: ^6.6.0 + mobx-react-lite: 3.4.0 preact: ^10.5.14 - preact-context-menu: 0.4.0-patch.0 + preact-context-menu: 0.4.1 preact-i18n: ^2.4.0-preactx prettier: ^2.3.1 - prismjs: ^1.23.0 + prismjs: ^1.28.0 + qrcode.react: ^3.0.2 react-beautiful-dnd: ^13.1.0 - react-device-detect: ^1.17.0 + react-device-detect: 2.2.2 react-helmet: ^6.1.0 react-hook-form: 6.3.0 react-overlapping-panels: 1.2.2 react-router-dom: ^5.2.0 react-scroll: ^1.8.2 - react-virtuoso: ^1.10.4 - revolt.js: 6.0.0-2 + react-virtuoso: ^2.12.0 + rehype-katex: ^6.0.2 + rehype-prism: ^2.1.3 + rehype-react: ^7.1.1 + remark-breaks: ^3.0.2 + remark-gfm: ^3.0.1 + remark-math: ^5.1.1 + remark-parse: ^10.0.1 + remark-rehype: ^10.1.0 + revolt.js: ^6.0.6 rimraf: ^3.0.2 sass: ^1.35.1 + semver: ^7.3.7 shade-blend-color: ^1.0.0 sirv-cli: ^1.0.14 + slate: ^0.81.1 + slate-history: ^0.66.0 + slate-react: ^0.81.0 stacktrace-js: ^2.0.2 styled-components: ^5.3.0 typescript: ^4.4.2 ulid: ^2.3.0 + unified: ^10.1.2 + unist-util-visit: ^4.1.0 use-resize-observer: ^7.0.0 vite: ^2.6.14 vite-plugin-pwa: ^0.11.13 @@ -3533,6 +3792,13 @@ __metadata: languageName: node linkType: hard +"comma-separated-tokens@npm:^2.0.0": + version: 2.0.2 + resolution: "comma-separated-tokens@npm:2.0.2" + checksum: 8fa68ff2605233571536a802a7c712b0c766e0c5088e067be72740054e84d040865eea945c984924ae84932bcc3e25a99f71601220b438e875b5f42b87277767 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -3547,6 +3813,20 @@ __metadata: languageName: node linkType: hard +"commander@npm:^8.0.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0 + languageName: node + linkType: hard + +"comment-parser@npm:1.3.1": + version: 1.3.1 + resolution: "comment-parser@npm:1.3.1" + checksum: 421e6a113a3afd548500e7174ab46a2049dccf92e82bbaa3b209031b1bdf97552aabfa1ae2a120c0b62df17e1ba70e0d8b05d68504fee78e1ef974c59bcfe718 + languageName: node + linkType: hard + "common-tags@npm:^1.8.0": version: 1.8.0 resolution: "common-tags@npm:1.8.0" @@ -3554,6 +3834,13 @@ __metadata: languageName: node linkType: hard +"compute-scroll-into-view@npm:^1.0.17": + version: 1.0.17 + resolution: "compute-scroll-into-view@npm:1.0.17" + checksum: b20c05a10c37813c5a6e4bf053c00f65c88d23afed7a6bd7a2a69e05e2ffc2df3483ecfe407d36bf16b8cec8be21ae1966c9c523093a03117e567156cd79a51e + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -3655,6 +3942,13 @@ __metadata: languageName: node linkType: hard +"css-selector-parser@npm:^1.0.0": + version: 1.4.1 + resolution: "css-selector-parser@npm:1.4.1" + checksum: 31948754e579eedb918c2fb2d5a4c643ec769ff4a0d03a7bd10b43b25d44973f8cbe86d7ec00c4494269f7ff38b3d2ab0f6ea801cece0ef0974e74469dff770c + languageName: node + linkType: hard + "css-to-react-native@npm:^3.0.0": version: 3.0.0 resolution: "css-to-react-native@npm:3.0.0" @@ -3680,7 +3974,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -3716,6 +4010,15 @@ __metadata: languageName: node linkType: hard +"decode-named-character-reference@npm:^1.0.0": + version: 1.0.2 + resolution: "decode-named-character-reference@npm:1.0.2" + dependencies: + character-entities: ^2.0.0 + checksum: f4c71d3b93105f20076052f9cb1523a22a9c796b8296cd35eef1ca54239c78d182c136a848b83ff8da2071e3ae2b1d300bf29d00650a6d6e675438cc31b11d78 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.3 resolution: "deep-is@npm:0.1.3" @@ -3753,6 +4056,13 @@ __metadata: languageName: node linkType: hard +"dequal@npm:^2.0.0": + version: 2.0.2 + resolution: "dequal@npm:2.0.2" + checksum: 86c7a2c59f7b0797ed397c74b5fcdb744e48fc19440b70ad6ac59f57550a96b0faef3f1cfd5760ec5e6d3f7cb101f634f1f80db4e727b1dc8389bf62d977c0a0 + languageName: node + linkType: hard + "detect-browser@npm:^5.2.0": version: 5.2.0 resolution: "detect-browser@npm:5.2.0" @@ -3760,6 +4070,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.0.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -3769,6 +4086,15 @@ __metadata: languageName: node linkType: hard +"direction@npm:^1.0.3": + version: 1.0.4 + resolution: "direction@npm:1.0.4" + bin: + direction: cli.js + checksum: 572ac399093d7c9f2181c96828d252922e2a962b8f31a7fc118e3f7619592c566cc2ed313baf7703f17b2be00cd3c1402550140d0c3f4f70362976376a08b095 + languageName: node + linkType: hard + "dlv@npm:^1.1.3": version: 1.1.3 resolution: "dlv@npm:1.1.3" @@ -3812,10 +4138,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.17": - version: 1.4.45 - resolution: "electron-to-chromium@npm:1.4.45" - checksum: 8afc465bfe4873701c748626bcd3081976526b3cd9cf4b098b0a1ad90bd2be5f4933ddf1e061cc140d2793146f0901f2b96996aaecd726d0abdebcaf5d3bdbaa +"electron-to-chromium@npm:^1.4.118": + version: 1.4.140 + resolution: "electron-to-chromium@npm:1.4.140" + checksum: bf06151bdd76dbcf00c97215d0c79479a4d2116e4a1734ee319cf83865ceab56ee834b3f4347bf9c01ae5c0a953fb0b93e2f097c3ed33f6292d03bcb40af651d languageName: node linkType: hard @@ -3844,13 +4170,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:~2.1.0": - version: 2.1.0 - resolution: "entities@npm:2.1.0" - checksum: a10a877e489586a3f6a691fe49bf3fc4e58f06c8e80522f08214a5150ba457e7017b447d4913a3fa041bda06ee4c92517baa4d8d75373eaa79369e9639225ffd - languageName: node - linkType: hard - "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -4149,6 +4468,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + "eslint-config-preact@npm:^1.1.4": version: 1.1.4 resolution: "eslint-config-preact@npm:1.1.4" @@ -4193,6 +4519,32 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jsdoc@npm:^39.3.2": + version: 39.3.2 + resolution: "eslint-plugin-jsdoc@npm:39.3.2" + dependencies: + "@es-joy/jsdoccomment": ~0.31.0 + comment-parser: 1.3.1 + debug: ^4.3.4 + escape-string-regexp: ^4.0.0 + esquery: ^1.4.0 + semver: ^7.3.7 + spdx-expression-parse: ^3.0.1 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 2fd3adb23f97c5cc8c03bd8c7338c12074e4e6d49eaee042db65317a69abd389a4c4992fbc9075fa3deabd1d89393b639683f612deac06d89950767571c03457 + languageName: node + linkType: hard + +"eslint-plugin-mobx@npm:^0.0.8": + version: 0.0.8 + resolution: "eslint-plugin-mobx@npm:0.0.8" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: c4d9977ccbde2041ad4fec405b36763cd0bc738c7cc4bde6054b560d4b0d3929371927fedf5a37f9b039c712a382f62455931ca255a142fd9089ce8b53d0d66b + languageName: node + linkType: hard + "eslint-plugin-react-hooks@npm:^4.2.0": version: 4.2.0 resolution: "eslint-plugin-react-hooks@npm:4.2.0" @@ -4414,6 +4766,13 @@ __metadata: languageName: node linkType: hard +"extend@npm:^3.0.0": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: a50a8309ca65ea5d426382ff09f33586527882cf532931cb08ca786ea3146c0553310bda688710ff61d7668eba9f96b923fe1420cdf56a2c3eaf30fcab87b515 + languageName: node + linkType: hard + "fake-mediastreamtrack@npm:^1.1.6": version: 1.1.6 resolution: "fake-mediastreamtrack@npm:1.1.6" @@ -4855,14 +5214,88 @@ __metadata: languageName: node linkType: hard -"highlight.js@npm:^10.7.2": - version: 10.7.3 - resolution: "highlight.js@npm:10.7.3" - checksum: defeafcd546b535d710d8efb8e650af9e3b369ef53e28c3dc7893eacfe263200bba4c5fcf43524ae66d5c0c296b1af0870523ceae3e3104d24b7abf6374a4fea +"hast-to-hyperscript@npm:^10.0.0": + version: 10.0.1 + resolution: "hast-to-hyperscript@npm:10.0.1" + dependencies: + "@types/unist": ^2.0.0 + comma-separated-tokens: ^2.0.0 + property-information: ^6.0.0 + space-separated-tokens: ^2.0.0 + style-to-object: ^0.3.0 + unist-util-is: ^5.0.0 + web-namespaces: ^2.0.0 + checksum: 0ec7a6f873312421c6cfa84f8c842fa00c74e96018c371ace4800fda6590e208db8e31d4e84b09e436fe6b9b87b2fd2968b30c27881ff82fc9fe466a0f59b922 languageName: node linkType: hard -"history@npm:^4.9.0": +"hast-util-from-parse5@npm:^7.0.0": + version: 7.1.0 + resolution: "hast-util-from-parse5@npm:7.1.0" + dependencies: + "@types/hast": ^2.0.0 + "@types/parse5": ^6.0.0 + "@types/unist": ^2.0.0 + hastscript: ^7.0.0 + property-information: ^6.0.0 + vfile: ^5.0.0 + vfile-location: ^4.0.0 + web-namespaces: ^2.0.0 + checksum: 4a774700042e03aeecca6b6977f0e915069eefcf81c30d59ae0e1d2d7170e419065bcd8708504cb7b4d19b05367daee2177ddce47db1b5a654bb7ec19ba8d227 + languageName: node + linkType: hard + +"hast-util-is-element@npm:^2.0.0": + version: 2.1.2 + resolution: "hast-util-is-element@npm:2.1.2" + dependencies: + "@types/hast": ^2.0.0 + "@types/unist": ^2.0.0 + checksum: c5fe9f7cde3775d4cbe19a9a55631a80b7a4ea0131fc2e3d097ebe228a35f09b9219f64b788b7a9cf819e6dcb6d1fc7830fd2f10ad536649e436e8c83da41e00 + languageName: node + linkType: hard + +"hast-util-parse-selector@npm:^3.0.0": + version: 3.1.0 + resolution: "hast-util-parse-selector@npm:3.1.0" + dependencies: + "@types/hast": ^2.0.0 + checksum: 8be1a2334652866b40fde72a8b7d0867a791ce8a70d15fd7bb44b9a4f349913b77999e5add41900466bc9461c6b0fdea391875ef534b33cacf7a2aee9d8e447c + languageName: node + linkType: hard + +"hast-util-to-text@npm:^3.1.0": + version: 3.1.1 + resolution: "hast-util-to-text@npm:3.1.1" + dependencies: + "@types/hast": ^2.0.0 + hast-util-is-element: ^2.0.0 + unist-util-find-after: ^4.0.0 + checksum: 2312a818c8ec7b02307b04175357e5a7a9918f48624d05366668ba60918734ca62b0ee21006a2a448e0e5a198654cd1fa4ba8c813702b465cb487e2320db523a + languageName: node + linkType: hard + +"hast-util-whitespace@npm:^2.0.0": + version: 2.0.0 + resolution: "hast-util-whitespace@npm:2.0.0" + checksum: abeb5386075bfb0facfce89eed0e13d2cb27a0910cec8fd234b48821a1538387a73fa7f458842e8c404148dc69434acbc10488d75b02817e460652c2c894c024 + languageName: node + linkType: hard + +"hastscript@npm:^7.0.0": + version: 7.0.2 + resolution: "hastscript@npm:7.0.2" + dependencies: + "@types/hast": ^2.0.0 + comma-separated-tokens: ^2.0.0 + hast-util-parse-selector: ^3.0.0 + property-information: ^6.0.0 + space-separated-tokens: ^2.0.0 + checksum: ee33aff714b12f9f83049550956c7fb3e5ac7bdd20e77b57dc01b66de06e8bb0b3ba24153d4b6a1d7fa660bfef91125ac29e1bb04fb628e30d11097d28037235 + languageName: node + linkType: hard + +"history@npm:4, history@npm:^4.9.0": version: 4.10.1 resolution: "history@npm:4.10.1" dependencies: @@ -4959,6 +5392,13 @@ __metadata: languageName: node linkType: hard +"immer@npm:^9.0.6": + version: 9.0.15 + resolution: "immer@npm:9.0.15" + checksum: 92e3d63e810e3c3c2bb61b70c45443e37ef983ad12924e3edaf03725ae5979618f5b473439bb3bb4a8c4769f25132f18dec10ea15c40f0b20da5691ff96ff611 + languageName: node + linkType: hard + "import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -5007,6 +5447,13 @@ __metadata: languageName: node linkType: hard +"inline-style-parser@npm:0.1.1": + version: 0.1.1 + resolution: "inline-style-parser@npm:0.1.1" + checksum: 5d545056a3e1f2bf864c928a886a0e1656a3517127d36917b973de581bd54adc91b4bf1febcb0da054f204b4934763f1a4e09308b4d55002327cf1d48ac5d966 + languageName: node + linkType: hard + "internal-slot@npm:^1.0.3": version: 1.0.3 resolution: "internal-slot@npm:1.0.3" @@ -5060,6 +5507,13 @@ __metadata: languageName: node linkType: hard +"is-buffer@npm:^2.0.0": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 + languageName: node + linkType: hard + "is-callable@npm:^1.1.4, is-callable@npm:^1.2.3, is-callable@npm:^1.2.4": version: 1.2.4 resolution: "is-callable@npm:1.2.4" @@ -5108,6 +5562,13 @@ __metadata: languageName: node linkType: hard +"is-hotkey@npm:^0.1.6": + version: 0.1.8 + resolution: "is-hotkey@npm:0.1.8" + checksum: 793d0cccaf804583d8f4b835e040d4b6a74cb3f8d4094cb8188ed7cc86e6edf8f650fc80fabd4309e6c29938f5b0e9a17b45504b29dbf92735ef8d780d613aa0 + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -5152,6 +5613,20 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^4.0.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 6dc45da70d04a81f35c9310971e78a6a3c7a63547ef782e3a07ee3674695081b6ca4e977fbb8efc48dae3375e0b34558d2bcd722aec9bddfa2d7db5b041be8ce + languageName: node + linkType: hard + +"is-plain-object@npm:^5.0.0": + version: 5.0.0 + resolution: "is-plain-object@npm:5.0.0" + checksum: e32d27061eef62c0847d303125440a38660517e586f2f3db7c9d179ae5b6674ab0f469d519b2e25c147a1a3bc87156d0d5f4d8821e0ce4a9ee7fe1fcf11ce45c + languageName: node + linkType: hard + "is-regex@npm:^1.1.3, is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -5286,6 +5761,13 @@ __metadata: languageName: node linkType: hard +"jsdoc-type-pratt-parser@npm:~3.1.0": + version: 3.1.0 + resolution: "jsdoc-type-pratt-parser@npm:3.1.0" + checksum: 2f437b57621f1e481918165f6cf0e48256628a9e510d8b3f88a2ab667bf2128bf8b94c628b57c43e78f555ca61983e9c282814703840dc091d2623992214a061 + languageName: node + linkType: hard + "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -5357,6 +5839,15 @@ __metadata: languageName: node linkType: hard +"json5@npm:^2.2.1": + version: 2.2.1 + resolution: "json5@npm:2.2.1" + bin: + json5: lib/cli.js + checksum: 74b8a23b102a6f2bf2d224797ae553a75488b5adbaee9c9b6e5ab8b510a2fc6e38f876d4c77dea672d4014a44b2399e15f2051ac2b37b87f74c0c7602003543b + languageName: node + linkType: hard + "jsonfile@npm:^6.0.1": version: 6.1.0 resolution: "jsonfile@npm:6.1.0" @@ -5387,6 +5878,17 @@ __metadata: languageName: node linkType: hard +"katex@npm:^0.13.0": + version: 0.13.24 + resolution: "katex@npm:0.13.24" + dependencies: + commander: ^8.0.0 + bin: + katex: cli.js + checksum: 1b7c8295867073d0db4f6fb41ef1c0e3418b8e23924ff61b446b36134cb74cdadc7242dfbfb922d9c32f0b15eda6160a08cd30948c4e78141966ca2991a1726b + languageName: node + linkType: hard + "katex@npm:^0.13.9": version: 0.13.16 resolution: "katex@npm:0.13.16" @@ -5398,6 +5900,17 @@ __metadata: languageName: node linkType: hard +"katex@npm:^0.15.0": + version: 0.15.6 + resolution: "katex@npm:0.15.6" + dependencies: + commander: ^8.0.0 + bin: + katex: cli.js + checksum: 2da808bbd1d3be27715006cd86767dd3fcce3e317fb3bbd64d407328d2d90de17b5d83062b2cfd0e0d0de32e340efbac214862bc96892a5d1492462e553728d4 + languageName: node + linkType: hard + "klaw@npm:^3.0.0": version: 3.0.0 resolution: "klaw@npm:3.0.0" @@ -5414,6 +5927,13 @@ __metadata: languageName: node linkType: hard +"kleur@npm:^4.0.3": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 1dc476e32741acf0b1b5b0627ffd0d722e342c1b0da14de3e8ae97821327ca08f9fb944542fb3c126d90ac5f27f9d804edbe7c585bf7d12ef495d115e0f22c12 + languageName: node + linkType: hard + "kolorist@npm:^1.2.10": version: 1.5.0 resolution: "kolorist@npm:1.5.0" @@ -5454,15 +5974,6 @@ __metadata: languageName: node linkType: hard -"linkify-it@npm:^3.0.1": - version: 3.0.2 - resolution: "linkify-it@npm:3.0.2" - dependencies: - uc.micro: ^1.0.1 - checksum: 08e14854ec3c29e3578311b1cd95e469952a27f191633189a23890628939fc45c6d84fa4495abb9f7f06e60f73a31b8881d834214864d46db914a09ffc7889ae - languageName: node - linkType: hard - "local-access@npm:^1.0.1": version: 1.1.0 resolution: "local-access@npm:1.1.0" @@ -5558,7 +6069,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20": +"lodash@npm:4.17.21, lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -5572,6 +6083,13 @@ __metadata: languageName: node linkType: hard +"longest-streak@npm:^3.0.0": + version: 3.0.1 + resolution: "longest-streak@npm:3.0.1" + checksum: 3b59c4c04ce3c70f137e339c10d574026fa3a711c45dc0e69a63a2c0ac981e57f837e1d5b64b991eee5234c4fa46fa10886a20626fb739ed3b04b77bcf6d14a8 + languageName: node + linkType: hard + "loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -5632,29 +6150,177 @@ __metadata: languageName: node linkType: hard -"markdown-it-emoji@npm:^2.0.0": - version: 2.0.0 - resolution: "markdown-it-emoji@npm:2.0.0" - checksum: 7d25844134d98a4e2cf70d9a14ea4ec5afc3f64740d69c88012795c8a24ed2f286850b72fd4437d60d2fe34f73cc1889a7e8e3ab4663f4fc3a4899991601bbba +"markdown-table@npm:^3.0.0": + version: 3.0.2 + resolution: "markdown-table@npm:3.0.2" + checksum: 7bd9eb54e7ac15165f79730ac6357b8194294552f727bcb34e29f3f1b72823c1220cb61153ebf0962c8faac4d25e49c62e8e9471cd6352a67cdca99928ecade1 languageName: node linkType: hard -"markdown-it@npm:^12.0.6": - version: 12.2.0 - resolution: "markdown-it@npm:12.2.0" +"mdast-util-definitions@npm:^5.0.0": + version: 5.1.1 + resolution: "mdast-util-definitions@npm:5.1.1" dependencies: - argparse: ^2.0.1 - entities: ~2.1.0 - linkify-it: ^3.0.1 - mdurl: ^1.0.1 - uc.micro: ^1.0.5 - bin: - markdown-it: bin/markdown-it.js - checksum: 8e3d6646edf8e7ef19ed707c59d16741bd40804f1e7567407efd2f346ae0f7ddcdeada83e7affebd21b9d7d947b27fc60fd795a970461785030a4e52e750122b + "@types/mdast": ^3.0.0 + "@types/unist": ^2.0.0 + unist-util-visit: ^4.0.0 + checksum: f8025e2c35f6f8641528037abe18f492ef100e00a48c92cf78b7a313f9ccdb0e30c6aed0b40539767a3f425be09e78cb0f2f9bc4131fff41ea4664a1a7314a14 languageName: node linkType: hard -"mdurl@npm:^1.0.1": +"mdast-util-find-and-replace@npm:^2.0.0": + version: 2.2.0 + resolution: "mdast-util-find-and-replace@npm:2.2.0" + dependencies: + escape-string-regexp: ^5.0.0 + unist-util-is: ^5.0.0 + unist-util-visit-parents: ^5.0.0 + checksum: 1ca772fcecc07a1c61c115df1185b4454c830f7f5c7c5bcf34957af58af6b93c355c6d324afa8f6de33c4ad1a338e426ff391ffb7e4686a6deeae091a4c0eeaa + languageName: node + linkType: hard + +"mdast-util-from-markdown@npm:^1.0.0": + version: 1.2.0 + resolution: "mdast-util-from-markdown@npm:1.2.0" + dependencies: + "@types/mdast": ^3.0.0 + "@types/unist": ^2.0.0 + decode-named-character-reference: ^1.0.0 + mdast-util-to-string: ^3.1.0 + micromark: ^3.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-decode-string: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + unist-util-stringify-position: ^3.0.0 + uvu: ^0.5.0 + checksum: fadc3521a3d95f4adbadad462ca27c28b3bfe08740ae158dc0c4a22329bf5593254d98b8fd4024ecad8c47c77ec275454dfacfb907ff1b98ff8f5de25c716d40 + languageName: node + linkType: hard + +"mdast-util-gfm-autolink-literal@npm:^1.0.0": + version: 1.0.2 + resolution: "mdast-util-gfm-autolink-literal@npm:1.0.2" + dependencies: + "@types/mdast": ^3.0.0 + ccount: ^2.0.0 + mdast-util-find-and-replace: ^2.0.0 + micromark-util-character: ^1.0.0 + checksum: 75e12f21ec24552ba33725f69a06cd703e5586d2296ca9d180927b2293c036e1bd39108adba83e8cbbefcc45ffd8821fb561b4c107684ed87bd9e5e286ba03bd + languageName: node + linkType: hard + +"mdast-util-gfm-footnote@npm:^1.0.0": + version: 1.0.1 + resolution: "mdast-util-gfm-footnote@npm:1.0.1" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-markdown: ^1.3.0 + micromark-util-normalize-identifier: ^1.0.0 + checksum: 4caf69058b438c9e34004acfb1d2b20d58306898d760b889f73d27ed5702cd940be9fcb2a08f6e58b8d9d8e2b1c886c549cd7d23b659da5fb2ed87a22f44c13c + languageName: node + linkType: hard + +"mdast-util-gfm-strikethrough@npm:^1.0.0": + version: 1.0.1 + resolution: "mdast-util-gfm-strikethrough@npm:1.0.1" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-markdown: ^1.3.0 + checksum: ce81222ab4c130516278f8db57be23bd529e9f8c30bb16ab5b2bf294c0dfd57f2dc7a010deede65f349a8d37be73f90dbaecd962f76f70befa8f43bcd32fe5b9 + languageName: node + linkType: hard + +"mdast-util-gfm-table@npm:^1.0.0": + version: 1.0.4 + resolution: "mdast-util-gfm-table@npm:1.0.4" + dependencies: + markdown-table: ^3.0.0 + mdast-util-from-markdown: ^1.0.0 + mdast-util-to-markdown: ^1.3.0 + checksum: 56d9f0376b3da3e4cc0f5047d62a4eefa765934a1084822bc7804e7cf93c458c4bff2a917fa4e89c917287431a7284b656bf23ef89553e943d7f853ffefae693 + languageName: node + linkType: hard + +"mdast-util-gfm-task-list-item@npm:^1.0.0": + version: 1.0.1 + resolution: "mdast-util-gfm-task-list-item@npm:1.0.1" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-markdown: ^1.3.0 + checksum: 9bb0f162532f8e11e571802ed19301572479fe9507652c8fb3f648279bbde3baa9f6377d9492dbba61eedd96755f8aff9c7c259287875544fb751907d79da69e + languageName: node + linkType: hard + +"mdast-util-gfm@npm:^2.0.0": + version: 2.0.1 + resolution: "mdast-util-gfm@npm:2.0.1" + dependencies: + mdast-util-from-markdown: ^1.0.0 + mdast-util-gfm-autolink-literal: ^1.0.0 + mdast-util-gfm-footnote: ^1.0.0 + mdast-util-gfm-strikethrough: ^1.0.0 + mdast-util-gfm-table: ^1.0.0 + mdast-util-gfm-task-list-item: ^1.0.0 + mdast-util-to-markdown: ^1.0.0 + checksum: 8b39e6694521094ae28d12cbeff074ef3ec3f7f7ec59fbddd4e8a45a275e092c6ba6ecee4c720938eb3ee072ebd41d743b08cc0ab9171612a5aeddc1e78ae882 + languageName: node + linkType: hard + +"mdast-util-math@npm:^2.0.0": + version: 2.0.1 + resolution: "mdast-util-math@npm:2.0.1" + dependencies: + "@types/mdast": ^3.0.0 + longest-streak: ^3.0.0 + mdast-util-to-markdown: ^1.3.0 + checksum: 7576b466276717198fc461501d40c930be50b1754ca82979bd16df08e5fb7b959587c2090fbe044f050cf3f24c5c6febf3756a96031ecabe4b82f11d38c74546 + languageName: node + linkType: hard + +"mdast-util-to-hast@npm:^12.1.0, mdast-util-to-hast@npm:^12.1.2": + version: 12.1.2 + resolution: "mdast-util-to-hast@npm:12.1.2" + dependencies: + "@types/hast": ^2.0.0 + "@types/mdast": ^3.0.0 + "@types/mdurl": ^1.0.0 + mdast-util-definitions: ^5.0.0 + mdurl: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + trim-lines: ^3.0.0 + unist-builder: ^3.0.0 + unist-util-generated: ^2.0.0 + unist-util-position: ^4.0.0 + unist-util-visit: ^4.0.0 + checksum: 7bb888e73ce2ffc67d57868f3f7190fc8f30dda7b1293a14dd9b8fe6e72432f63db5c1e3015f9743c9f5bb17b33f622e3cea60bd54679395c9bc1949c16e4ce7 + languageName: node + linkType: hard + +"mdast-util-to-markdown@npm:^1.0.0, mdast-util-to-markdown@npm:^1.3.0": + version: 1.3.0 + resolution: "mdast-util-to-markdown@npm:1.3.0" + dependencies: + "@types/mdast": ^3.0.0 + "@types/unist": ^2.0.0 + longest-streak: ^3.0.0 + mdast-util-to-string: ^3.0.0 + micromark-util-decode-string: ^1.0.0 + unist-util-visit: ^4.0.0 + zwitch: ^2.0.0 + checksum: 0ea4fc11b7a49b15d400d50044429c45222cb9bc583553288c7c54704d051f25049233817129ba56a6f581f1e20916e5c540870a80987318747a95b44a36ba3e + languageName: node + linkType: hard + +"mdast-util-to-string@npm:^3.0.0, mdast-util-to-string@npm:^3.1.0": + version: 3.1.0 + resolution: "mdast-util-to-string@npm:3.1.0" + checksum: f42ddd4e22f2215a75715b92ea6e3149c4ba356e7781d7b94fc86ded1c79cec3f986afeecef3a4a80068c9b224a6520099783a12146b957de24f020a3e47dd29 + languageName: node + linkType: hard + +"mdurl@npm:^1.0.0": version: 1.0.1 resolution: "mdurl@npm:1.0.1" checksum: 71731ecba943926bfbf9f9b51e28b5945f9411c4eda80894221b47cc105afa43ba2da820732b436f0798fd3edbbffcd1fc1415843c41a87fea08a41cc1e3d02b @@ -5700,6 +6366,352 @@ __metadata: languageName: node linkType: hard +"micromark-core-commonmark@npm:^1.0.0, micromark-core-commonmark@npm:^1.0.1": + version: 1.0.6 + resolution: "micromark-core-commonmark@npm:1.0.6" + dependencies: + decode-named-character-reference: ^1.0.0 + micromark-factory-destination: ^1.0.0 + micromark-factory-label: ^1.0.0 + micromark-factory-space: ^1.0.0 + micromark-factory-title: ^1.0.0 + micromark-factory-whitespace: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-chunked: ^1.0.0 + micromark-util-classify-character: ^1.0.0 + micromark-util-html-tag-name: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-subtokenize: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.1 + uvu: ^0.5.0 + checksum: 4b483c46077f696ed310f6d709bb9547434c218ceb5c1220fde1707175f6f68b44da15ab8668f9c801e1a123210071e3af883a7d1215122c913fd626f122bfc2 + languageName: node + linkType: hard + +"micromark-extension-gfm-autolink-literal@npm:^1.0.0": + version: 1.0.3 + resolution: "micromark-extension-gfm-autolink-literal@npm:1.0.3" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: bb181972ac346ca73ca1ab0b80b80c9d6509ed149799d2217d5442670f499c38a94edff73d32fa52b390d89640974cfbd7f29e4ad7d599581d5e1cabcae636a2 + languageName: node + linkType: hard + +"micromark-extension-gfm-footnote@npm:^1.0.0": + version: 1.0.4 + resolution: "micromark-extension-gfm-footnote@npm:1.0.4" + dependencies: + micromark-core-commonmark: ^1.0.0 + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 8daa203f5cf753338d5ecdbaae6b3ab6319d34b6013b90ea6860bed299418cecf86e69e48dabe42562e334760c738c77c5acdb47e75ae26f5f01f02f3bf0952d + languageName: node + linkType: hard + +"micromark-extension-gfm-strikethrough@npm:^1.0.0": + version: 1.0.4 + resolution: "micromark-extension-gfm-strikethrough@npm:1.0.4" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-classify-character: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: f43d316b85fe93df1711cdcdc99a5320b941239349234bd262fc708cb67ad47bdfb41d1a7ebe2a5829816b0e9d3107380a5c1e558cb536a75354cbe4857823ba + languageName: node + linkType: hard + +"micromark-extension-gfm-table@npm:^1.0.0": + version: 1.0.5 + resolution: "micromark-extension-gfm-table@npm:1.0.5" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: f0aab3b4333cc24b1534b08dc4cce986dd606df8b7ed913e5a1de9fe2d3ae67b2435663c0bc271b528874af4928e580e1ad540ea9117d7f2d74edb28859c97ef + languageName: node + linkType: hard + +"micromark-extension-gfm-tagfilter@npm:^1.0.0": + version: 1.0.1 + resolution: "micromark-extension-gfm-tagfilter@npm:1.0.1" + dependencies: + micromark-util-types: ^1.0.0 + checksum: 63e8d68f25871722900a67a8001d5da21f19ea707f3566fc7d0b2eb1f6d52476848bb6a41576cf22470565124af9497c5aae842355faa4c14ec19cb1847e71ec + languageName: node + linkType: hard + +"micromark-extension-gfm-task-list-item@npm:^1.0.0": + version: 1.0.3 + resolution: "micromark-extension-gfm-task-list-item@npm:1.0.3" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: d320b0c5301f87e211c06a2330d1ee0fee6da14f0d6d44d5211055b465dadff34390cd6b258a5e0ca376fcda3364fef9a12fe6e26a0c858231fa3b98ddbf7785 + languageName: node + linkType: hard + +"micromark-extension-gfm@npm:^2.0.0": + version: 2.0.1 + resolution: "micromark-extension-gfm@npm:2.0.1" + dependencies: + micromark-extension-gfm-autolink-literal: ^1.0.0 + micromark-extension-gfm-footnote: ^1.0.0 + micromark-extension-gfm-strikethrough: ^1.0.0 + micromark-extension-gfm-table: ^1.0.0 + micromark-extension-gfm-tagfilter: ^1.0.0 + micromark-extension-gfm-task-list-item: ^1.0.0 + micromark-util-combine-extensions: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: b181479c87be38d5ae8d28e6dc52fab73c894fd2706876746f27a91fb186644ce03532a9c35dca2186327a0e2285cd5242ad0361dc89adedd4a50376ffd94e22 + languageName: node + linkType: hard + +"micromark-extension-math@npm:^2.0.0": + version: 2.0.2 + resolution: "micromark-extension-math@npm:2.0.2" + dependencies: + "@types/katex": ^0.11.0 + katex: ^0.13.0 + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: c604d2e75443cd20988c485ecf35ff6799497b038d24e5c680107ebb6756031225df9292e449914178f04b837379b5f95dea0ad3fe4ae77ce60d194f102576a5 + languageName: node + linkType: hard + +"micromark-factory-destination@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-factory-destination@npm:1.0.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 8e733ae9c1c2342f14ff290bf09946e20f6f540117d80342377a765cac48df2ea5e748f33c8b07501ad7a43414b1a6597c8510ede2052b6bf1251fab89748e20 + languageName: node + linkType: hard + +"micromark-factory-label@npm:^1.0.0": + version: 1.0.2 + resolution: "micromark-factory-label@npm:1.0.2" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 957e9366bdc8dbc1437c0706ff96972fa985ab4b1274abcae12f6094f527cbf5c69e7f2304c23c7f4b96e311ff7911d226563b8b43dcfcd4091e8c985fb97ce6 + languageName: node + linkType: hard + +"micromark-factory-space@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-factory-space@npm:1.0.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 70d3aafde4e68ef4e509a3b644e9a29e4aada00801279e346577b008cbca06d78051bcd62aa7ea7425856ed73f09abd2b36607803055f726f52607ee7cb706b0 + languageName: node + linkType: hard + +"micromark-factory-title@npm:^1.0.0": + version: 1.0.2 + resolution: "micromark-factory-title@npm:1.0.2" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 9a9cf66babde0bad1e25d6c1087082bfde6dfc319a36cab67c89651cc1a53d0e21cdec83262b5a4c33bff49f0e3c8dc2a7bd464e991d40dbea166a8f9b37e5b2 + languageName: node + linkType: hard + +"micromark-factory-whitespace@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-factory-whitespace@npm:1.0.0" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 0888386e6ea2dd665a5182c570d9b3d0a172d3f11694ca5a2a84e552149c9f1429f5b975ec26e1f0fa4388c55a656c9f359ce5e0603aff6175ba3e255076f20b + languageName: node + linkType: hard + +"micromark-util-character@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-character@npm:1.1.0" + dependencies: + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 504a4e3321f69bddf3fec9f0c1058239fc23336bda5be31d532b150491eda47965a251b37f8a7a9db0c65933b3aaa49cf88044fb1028be3af7c5ee6212bf8d5f + languageName: node + linkType: hard + +"micromark-util-chunked@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-util-chunked@npm:1.0.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: c1efd56e8c4217bcf1c6f1a9fb9912b4a2a5503b00d031da902be922fb3fee60409ac53f11739991291357b2784fb0647ddfc74c94753a068646c0cb0fd71421 + languageName: node + linkType: hard + +"micromark-util-classify-character@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-util-classify-character@npm:1.0.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 180446e6a1dec653f625ded028f244784e1db8d10ad05c5d70f08af9de393b4a03dc6cf6fa5ed8ccc9c24bbece7837abf3bf66681c0b4adf159364b7d5236dfd + languageName: node + linkType: hard + +"micromark-util-combine-extensions@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-util-combine-extensions@npm:1.0.0" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 5304a820ef75340e1be69d6ad167055b6ba9a3bafe8171e5945a935752f462415a9dd61eb3490220c055a8a11167209a45bfa73f278338b7d3d61fa1464d3f35 + languageName: node + linkType: hard + +"micromark-util-decode-numeric-character-reference@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-util-decode-numeric-character-reference@npm:1.0.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: f3ae2bb582a80f1e9d3face026f585c0c472335c064bd850bde152376f0394cb2831746749b6be6e0160f7d73626f67d10716026c04c87f402c0dd45a1a28633 + languageName: node + linkType: hard + +"micromark-util-decode-string@npm:^1.0.0": + version: 1.0.2 + resolution: "micromark-util-decode-string@npm:1.0.2" + dependencies: + decode-named-character-reference: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-symbol: ^1.0.0 + checksum: 2dbb41c9691cc71505d39706405139fb7d6699429d577a524c7c248ac0cfd09d3dd212ad8e91c143a00b2896f26f81136edc67c5bda32d20446f0834d261b17a + languageName: node + linkType: hard + +"micromark-util-encode@npm:^1.0.0": + version: 1.0.1 + resolution: "micromark-util-encode@npm:1.0.1" + checksum: 9290583abfdc79ea3e7eb92c012c47a0e14327888f8aaa6f57ff79b3058d8e7743716b9d91abca3646f15ab3d78fdad9779fdb4ccf13349cd53309dfc845253a + languageName: node + linkType: hard + +"micromark-util-html-tag-name@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-html-tag-name@npm:1.1.0" + checksum: a9b783cec89ec813648d59799464c1950fe281ae797b2a965f98ad0167d7fa1a247718eff023b4c015f47211a172f9446b8e6b98aad50e3cd44a3337317dad2c + languageName: node + linkType: hard + +"micromark-util-normalize-identifier@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-util-normalize-identifier@npm:1.0.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: d7c09d5e8318fb72f194af72664bd84a48a2928e3550b2b21c8fbc0ec22524f2a72e0f6663d2b95dc189a6957d3d7759b60716e888909710767cd557be821f8b + languageName: node + linkType: hard + +"micromark-util-resolve-all@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-util-resolve-all@npm:1.0.0" + dependencies: + micromark-util-types: ^1.0.0 + checksum: 409667f2bd126ef8acce009270d2aecaaa5584c5807672bc657b09e50aa91bd2e552cf41e5be1e6469244a83349cbb71daf6059b746b1c44e3f35446fef63e50 + languageName: node + linkType: hard + +"micromark-util-sanitize-uri@npm:^1.0.0": + version: 1.0.0 + resolution: "micromark-util-sanitize-uri@npm:1.0.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-encode: ^1.0.0 + micromark-util-symbol: ^1.0.0 + checksum: 77448ec3a5d18f0ac975ea47591fbf0d5bd5568f9a0d033d9e318f90656031f037c5ff9137e93faf289480eaea70a5382e2571ebf9edcb1c1cd2a5187b6b3160 + languageName: node + linkType: hard + +"micromark-util-subtokenize@npm:^1.0.0": + version: 1.0.2 + resolution: "micromark-util-subtokenize@npm:1.0.2" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: c32ee58a7e1384ab1161a9ee02fbb04ad7b6e96d0b8c93dba9803c329a53d07f22ab394c7a96b2e30d6b8fbe3585b85817dba07277b1317111fc234e166bd2d1 + languageName: node + linkType: hard + +"micromark-util-symbol@npm:^1.0.0": + version: 1.0.1 + resolution: "micromark-util-symbol@npm:1.0.1" + checksum: c6a3023b3a7432c15864b5e33a1bcb5042ac7aa097f2f452e587bef45433d42d39e0a5cce12fbea91e0671098ba0c3f62a2b30ce1cde66ecbb5e8336acf4391d + languageName: node + linkType: hard + +"micromark-util-types@npm:^1.0.0, micromark-util-types@npm:^1.0.1": + version: 1.0.2 + resolution: "micromark-util-types@npm:1.0.2" + checksum: 08dc901b7c06ee3dfeb54befca05cbdab9525c1cf1c1080967c3878c9e72cb9856c7e8ff6112816e18ead36ce6f99d55aaa91560768f2f6417b415dcba1244df + languageName: node + linkType: hard + +"micromark@npm:^3.0.0": + version: 3.0.10 + resolution: "micromark@npm:3.0.10" + dependencies: + "@types/debug": ^4.0.0 + debug: ^4.0.0 + decode-named-character-reference: ^1.0.0 + micromark-core-commonmark: ^1.0.1 + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-chunked: ^1.0.0 + micromark-util-combine-extensions: ^1.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-encode: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + micromark-util-subtokenize: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.1 + uvu: ^0.5.0 + checksum: 04663fe0308cccfbf338111b41d3d82d6445d1d2b834c9fc1880e1ea3874c4a3b81adfafe62b0bc7708ba0a86889885ea31b4dbb39f1f72190c3aab46b743bb1 + languageName: node + linkType: hard + "micromatch@npm:^4.0.4": version: 4.0.4 resolution: "micromatch@npm:4.0.4" @@ -5845,18 +6857,18 @@ __metadata: languageName: node linkType: hard -"mobx-react-lite@npm:^3.3.0": - version: 3.3.0 - resolution: "mobx-react-lite@npm:3.3.0" +"mobx-react-lite@npm:3.4.0, mobx-react-lite@npm:^3.4.0": + version: 3.4.0 + resolution: "mobx-react-lite@npm:3.4.0" peerDependencies: mobx: ^6.1.0 - react: ^16.8.0 || ^17 + react: ^16.8.0 || ^17 || ^18 peerDependenciesMeta: react-dom: optional: true react-native: optional: true - checksum: 0f55bd2009a9cedc6b81d70b88b57dc4161362a16ba6ae0af341e673ca1c627bc3c4088c0cb13133e57e6fa6748b09b4c26aff7fab26c60ed95d27e939846fa3 + checksum: 9294e127e281c8b37ec7bcaf17de479f50519e6ad485b58d7b991291900511541a5a718653759d3cf6503462c70325d025e1c2ed376d4584fb1b2d3aac9d9b48 languageName: node linkType: hard @@ -5867,10 +6879,10 @@ __metadata: languageName: node linkType: hard -"mobx@npm:^6.5.0": - version: 6.5.0 - resolution: "mobx@npm:6.5.0" - checksum: 1210fb0b1c515b5f0ec2916296c32ca19b733e03b34f180af382d44b90668a15b4143c69bb06ca8785ebc3da3e761c6c60d0e72c945c199efc823088af1941ab +"mobx@npm:^6.6.0": + version: 6.6.0 + resolution: "mobx@npm:6.6.0" + checksum: 369b8d6830ec286e9c856c80002c7a554d46bca739b7f76432cb56b3ce1fe0a7ed7e5a994b9793d30023beeff0dd16300a501e831fa97107104e4fedc7d4af8f languageName: node linkType: hard @@ -5945,10 +6957,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.1": - version: 2.0.1 - resolution: "node-releases@npm:2.0.1" - checksum: b20dd8d4bced11f75060f0387e05e76b9dc4a0451f7bb3516eade6f50499ea7768ba95d8a60d520c193402df1e58cb3fe301510cc1c1ad68949c3d57b5149866 +"node-releases@npm:^2.0.3": + version: 2.0.5 + resolution: "node-releases@npm:2.0.5" + checksum: e85d949addd19f8827f32569d2be5751e7812ccf6cc47879d49f79b5234ff4982225e39a3929315f96370823b070640fb04d79fc0ddec8b515a969a03493a42f languageName: node linkType: hard @@ -5982,6 +6994,15 @@ __metadata: languageName: node linkType: hard +"nth-check@npm:^2.0.0": + version: 2.1.1 + resolution: "nth-check@npm:2.1.1" + dependencies: + boolbase: ^1.0.0 + checksum: 5afc3dafcd1573b08877ca8e6148c52abd565f1d06b1eb08caf982e3fa289a82f2cae697ffb55b5021e146d60443f1590a5d6b944844e944714a5b549675bcd3 + languageName: node + linkType: hard + "object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -6136,6 +7157,13 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^6.0.0": + version: 6.0.1 + resolution: "parse5@npm:6.0.1" + checksum: 7d569a176c5460897f7c8f3377eff640d54132b9be51ae8a8fa4979af940830b2b0c296ce75e5bd8f4041520aadde13170dbdec44889975f906098ea0002f4bd + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -6212,12 +7240,12 @@ __metadata: languageName: node linkType: hard -"preact-context-menu@npm:0.4.0-patch.0": - version: 0.4.0-patch.0 - resolution: "preact-context-menu@npm:0.4.0-patch.0" +"preact-context-menu@npm:0.4.1": + version: 0.4.1 + resolution: "preact-context-menu@npm:0.4.1" dependencies: preact: ^10.5.14 - checksum: ca2d429f9fc96c1f131f89cb2c72d0105bc3f52c05ad33288e5b0a56b52f1d1ed5d60ae2a0c56535047daaa02c0407c63aadf632a88cfcc4897db8a47123705d + checksum: 0816c1c2024527b5f1b67213d86c6ff4654f626ad19b8fd996dd606278f42f8b742a55d257abc0e79c6c5f0d9c2718a6326b399748b64ee17334158ca30a04f0 languageName: node linkType: hard @@ -6281,10 +7309,10 @@ __metadata: languageName: node linkType: hard -"prismjs@npm:^1.23.0": - version: 1.24.1 - resolution: "prismjs@npm:1.24.1" - checksum: e5d14a4ba56773122039295bd760c72106acc964e04cb9831b9ae7e7a58f67ccac6c053e77e21f1018a3684f31d35bb065c0c81fd4ff00b73b1570c3ace4aef0 +"prismjs@npm:^1.24.1, prismjs@npm:^1.28.0": + version: 1.28.0 + resolution: "prismjs@npm:1.28.0" + checksum: bde93fb2beb45b7243219fc53855f59ee54b3fa179f315e8f9d66244d756ef984462e10561bbdc6713d3d7e051852472d7c284f5794a8791eeaefea2fb910b16 languageName: node linkType: hard @@ -6323,6 +7351,13 @@ __metadata: languageName: node linkType: hard +"property-information@npm:^6.0.0": + version: 6.1.1 + resolution: "property-information@npm:6.1.1" + checksum: 654b1e5c3578e1d522bd22b7cf48881f5054789969ddbefea22e5359805fda5dbf0c5ef76bb26516da26fedac8752587ddc4c8f3b9e16bc0c6e7feb8b6086864 + languageName: node + linkType: hard + "punycode@npm:^2.1.0": version: 2.1.1 resolution: "punycode@npm:2.1.1" @@ -6330,6 +7365,15 @@ __metadata: languageName: node linkType: hard +"qrcode.react@npm:^3.0.2": + version: 3.0.2 + resolution: "qrcode.react@npm:3.0.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 4102e9f416d86808728b93dca4e90cab0b2d3eca2bfe501a26ca62237062ded2121711cfc4edf64832c63e04d34956e26c2e7088023949f9328bbaa56004777d + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -6371,15 +7415,15 @@ __metadata: languageName: node linkType: hard -"react-device-detect@npm:^1.17.0": - version: 1.17.0 - resolution: "react-device-detect@npm:1.17.0" +"react-device-detect@npm:2.2.2, react-device-detect@npm:^2.2.2": + version: 2.2.2 + resolution: "react-device-detect@npm:2.2.2" dependencies: - ua-parser-js: ^0.7.24 + ua-parser-js: ^1.0.2 peerDependencies: - react: ">= 0.14.0 < 18.0.0" - react-dom: ">= 0.14.0 < 18.0.0" - checksum: bd3583e392af0e807f5329c0763d4f4f15d211363c3cfbb5308221a874faf68a7d7ac339f3c0a4d0c5878e04eaf859e9a7405f2a8ee4a57e739ac7762c1907a1 + react: ">= 0.14.0" + react-dom: ">= 0.14.0" + checksum: d9245cf5a1c1e565e88523ed6be580497d1f6a972fb100a81092943bb7e44afdcdbae0d67bebe7424c4ba5b27a5d13df7894d122307f070fc26062704f7ec788 languageName: node linkType: hard @@ -6413,13 +7457,20 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.1": +"react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.1": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f languageName: node linkType: hard +"react-is@npm:^17.0.2": + version: 17.0.2 + resolution: "react-is@npm:17.0.2" + checksum: 9d6d111d8990dc98bc5402c1266a808b0459b5d54830bbea24c12d908b536df7883f268a7868cfaedde3dd9d4e0d574db456f84d2e6df9c4526f99bb4b5344d8 + languageName: node + linkType: hard + "react-overlapping-panels@npm:1.2.2": version: 1.2.2 resolution: "react-overlapping-panels@npm:1.2.2" @@ -6430,23 +7481,23 @@ __metadata: linkType: hard "react-redux@npm:^7.2.0": - version: 7.2.5 - resolution: "react-redux@npm:7.2.5" + version: 7.2.8 + resolution: "react-redux@npm:7.2.8" dependencies: - "@babel/runtime": ^7.12.1 - "@types/react-redux": ^7.1.16 + "@babel/runtime": ^7.15.4 + "@types/react-redux": ^7.1.20 hoist-non-react-statics: ^3.3.2 loose-envify: ^1.4.0 prop-types: ^15.7.2 - react-is: ^16.13.1 + react-is: ^17.0.2 peerDependencies: - react: ^16.8.3 || ^17 + react: ^16.8.3 || ^17 || ^18 peerDependenciesMeta: react-dom: optional: true react-native: optional: true - checksum: 04ac4a4178067cbcfc05506dfea9f7e01730093a5752f050567f7ae4a38c03c96da9d8fed051f8ab1ecede5ea8a15ee41c5f6c5eeb7f04f37e4d13e431ec7830 + checksum: ecf1933e91013f2d41bfc781515b536bf81eb1f70ff228607841094c8330fe77d522372b359687e51c0b52b9888dba73db9ac0486aace1896ab9eb9daec102d5 languageName: node linkType: hard @@ -6509,16 +7560,16 @@ __metadata: languageName: node linkType: hard -"react-virtuoso@npm:^1.10.4": - version: 1.11.0 - resolution: "react-virtuoso@npm:1.11.0" +"react-virtuoso@npm:^2.12.0": + version: 2.12.0 + resolution: "react-virtuoso@npm:2.12.0" dependencies: - "@virtuoso.dev/react-urx": ^0.2.5 - "@virtuoso.dev/urx": ^0.2.5 - resize-observer-polyfill: ^1.5.1 + "@virtuoso.dev/react-urx": ^0.2.12 + "@virtuoso.dev/urx": ^0.2.12 peerDependencies: - react: ">=16" - checksum: bd0ba533a0a8a318d1fe5bd082a32ffcf1447a22dc8c9affa0d77c4ac9d7e8874ea806387e135788632dc27e189b7c0c26e78994d21c5db6015d563247ed2655 + react: ">=16 || >=17 || >= 18" + react-dom: ">=16 || >=17 || >= 18" + checksum: b40309cb6d5175bcfa8a6c648af35f14e051b31e2cb811298cbc5c37dd5179f0a307c20fa9e7232b4179d1ff3fd31599c747249899d1da5d11afd8ee659d7368 languageName: node linkType: hard @@ -6543,11 +7594,11 @@ __metadata: linkType: hard "redux@npm:^4.0.0, redux@npm:^4.0.4": - version: 4.1.1 - resolution: "redux@npm:4.1.1" + version: 4.2.0 + resolution: "redux@npm:4.2.0" dependencies: "@babel/runtime": ^7.9.2 - checksum: 99519438a5d20b69404ad3816307ccc189f16df04b64c50d82c415ec488ea68b656d7a2fc81b6345e8d90f095344dfea68246500f72613d76464986660bc0485 + checksum: 75f3955c89b3f18edf5411e5fb482aa2e4f41a416183e8802a6bf6472c4fc3d47675b8b321d147f8af8e0f616436ac507bf5a25f1c4d6180e797b549c7db2c1d languageName: node linkType: hard @@ -6632,6 +7683,127 @@ __metadata: languageName: node linkType: hard +"rehype-katex@npm:^6.0.2": + version: 6.0.2 + resolution: "rehype-katex@npm:6.0.2" + dependencies: + "@types/hast": ^2.0.0 + "@types/katex": ^0.11.0 + hast-util-to-text: ^3.1.0 + katex: ^0.15.0 + rehype-parse: ^8.0.0 + unified: ^10.0.0 + unist-util-remove-position: ^4.0.0 + unist-util-visit: ^4.0.0 + checksum: ac8b3486441697b8e22cb7ebf6ec58e06d190240f45b128fe60422b9eb887599f38406581e6e3356af967eb1d45d631b0c09387f060190641f402f56c78fa771 + languageName: node + linkType: hard + +"rehype-parse@npm:^7 || ^ 8, rehype-parse@npm:^8.0.0": + version: 8.0.4 + resolution: "rehype-parse@npm:8.0.4" + dependencies: + "@types/hast": ^2.0.0 + hast-util-from-parse5: ^7.0.0 + parse5: ^6.0.0 + unified: ^10.0.0 + checksum: e678a5f9fa7cb91d5957f5f38bc37bc9fb90b8011a1ed6a90541ba6fff9f243c752c88b7f422cba8f5ba83ccb22942b1825654e8c3040970c703b85a6037efdf + languageName: node + linkType: hard + +"rehype-prism@npm:^2.1.3": + version: 2.1.3 + resolution: "rehype-prism@npm:2.1.3" + dependencies: + "@types/hast": "*" + "@types/mdast": "*" + "@types/node": ">12" + "@types/prismjs": ^1.16.6 + "@types/unist": "*" + prismjs: ^1.24.1 + rehype-parse: ^7 || ^ 8 + unist-util-is: ^4 || ^5 + unist-util-select: ^4 + unist-util-visit: ^3 || ^4 + peerDependencies: + unified: ^10 + checksum: d75864bd5b0e4b43403453fa77302956ad4b9841dd0bf201fa2258f2236c343a07306d28d465c3063e2e6eab5067467df3d24f49240acaf0c618637a33a525ab + languageName: node + linkType: hard + +"rehype-react@npm:^7.1.1": + version: 7.1.1 + resolution: "rehype-react@npm:7.1.1" + dependencies: + "@mapbox/hast-util-table-cell-style": ^0.2.0 + "@types/hast": ^2.0.0 + hast-to-hyperscript: ^10.0.0 + hast-util-whitespace: ^2.0.0 + unified: ^10.0.0 + peerDependencies: + "@types/react": ">=17" + checksum: 218b5e13776c6c0f5b430fe4c57de391f8417789b570c751f00b7c88da94ef08f7f869695189646ebc683155b02e55ac20c8e4148d4f2cbbcc39d0b05a66dca6 + languageName: node + linkType: hard + +"remark-breaks@npm:^3.0.2": + version: 3.0.2 + resolution: "remark-breaks@npm:3.0.2" + dependencies: + "@types/mdast": ^3.0.0 + unified: ^10.0.0 + unist-util-visit: ^4.0.0 + checksum: 5f46b18818f8a77e4fbc607c99736eedbe1f8cbad3d7390ce8359f08e3b749de8778ec8812287a41f51ffef2524b0be0dd623fcdbcda5de7f13f9902851f80b3 + languageName: node + linkType: hard + +"remark-gfm@npm:^3.0.1": + version: 3.0.1 + resolution: "remark-gfm@npm:3.0.1" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-gfm: ^2.0.0 + micromark-extension-gfm: ^2.0.0 + unified: ^10.0.0 + checksum: 02254f74d67b3419c2c9cf62d799ec35f6c6cd74db25c001361751991552a7ce86049a972107bff8122d85d15ae4a8d1a0618f3bc01a7df837af021ae9b2a04e + languageName: node + linkType: hard + +"remark-math@npm:^5.1.1": + version: 5.1.1 + resolution: "remark-math@npm:5.1.1" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-math: ^2.0.0 + micromark-extension-math: ^2.0.0 + unified: ^10.0.0 + checksum: 1baec5862e36bbb8645144a73740e63a3aed2d547a64b731bb1a0162658319679378fd70f3d3d534655c2a0fcc3f941adba31cca33808e134fa22202a5d314f9 + languageName: node + linkType: hard + +"remark-parse@npm:^10.0.1": + version: 10.0.1 + resolution: "remark-parse@npm:10.0.1" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-from-markdown: ^1.0.0 + unified: ^10.0.0 + checksum: 505088e564ab53ff054433368adbb7b551f69240c7d9768975529837a86f1d0f085e72d6211929c5c42db315273df4afc94f3d3a8662ffdb69468534c6643d29 + languageName: node + linkType: hard + +"remark-rehype@npm:^10.1.0": + version: 10.1.0 + resolution: "remark-rehype@npm:10.1.0" + dependencies: + "@types/hast": ^2.0.0 + "@types/mdast": ^3.0.0 + mdast-util-to-hast: ^12.1.0 + unified: ^10.0.0 + checksum: b9ac8acff3383b204dfdc2599d0bdf86e6ca7e837033209584af2e6aaa6a9013e519a379afa3201299798cab7298c8f4b388de118c312c67234c133318aec084 + languageName: node + linkType: hard + "require-from-string@npm:^2.0.2": version: 2.0.2 resolution: "require-from-string@npm:2.0.2" @@ -6639,13 +7811,6 @@ __metadata: languageName: node linkType: hard -"resize-observer-polyfill@npm:^1.5.1": - version: 1.5.1 - resolution: "resize-observer-polyfill@npm:1.5.1" - checksum: 57e7f79489867b00ba43c9c051524a5c8f162a61d5547e99333549afc23e15c44fd43f2f318ea0261ea98c0eb3158cca261e6f48d66e1ed1cd1f340a43977094 - languageName: node - linkType: hard - "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -6714,20 +7879,20 @@ __metadata: languageName: node linkType: hard -"revolt-api@npm:0.5.3-5-patch.3": - version: 0.5.3-5-patch.3 - resolution: "revolt-api@npm:0.5.3-5-patch.3" +"revolt-api@npm:0.5.4": + version: 0.5.4 + resolution: "revolt-api@npm:0.5.4" dependencies: - "@insertish/oapi": 0.1.15 + "@insertish/oapi": 0.1.16 axios: ^0.26.1 lodash.defaultsdeep: ^4.6.1 - checksum: 802d24359e64142317b16eaa40553f872f6ba4876e065704c7d7fb201f993b924076a770f74a49d95a4a494672bc4ab91284868d479c984b03688437749d4979 + checksum: bd40acabac1b6c5848b1d6e555297de5aa3e0950a4de67523c4cf986a8037380e3addc5e16babebc8dfa6570cd1d1957efe9a3aaa6a206b9286e5b7f5941d699 languageName: node linkType: hard -"revolt.js@npm:6.0.0-2": - version: 6.0.0-2 - resolution: "revolt.js@npm:6.0.0-2" +"revolt.js@npm:^6.0.6": + version: 6.0.6 + resolution: "revolt.js@npm:6.0.6" dependencies: "@insertish/exponential-backoff": 3.1.0-patch.2 "@insertish/isomorphic-ws": ^4.0.1 @@ -6738,10 +7903,10 @@ __metadata: lodash.isequal: ^4.5.0 long: ^5.2.0 mobx: ^6.3.2 - revolt-api: 0.5.3-5-patch.3 + revolt-api: 0.5.4 ulid: ^2.3.0 ws: ^8.2.2 - checksum: 4ca0991f33bc0fc610ff551dc10ba0eb785694dfe4c0fde82d63c99d1b89c1083a3d9e5c3ad28f165a5bc633b8ec4b5ecd432932a1df13fd44afaf52df8af325 + checksum: 079bdb983c650233378a617b771d7ff64396ce96fbd822fea20e9897fa14c2e589869e4a66f749dc74ce08218af425f97ab42fcaca7a3ab0f68f38f163484260 languageName: node linkType: hard @@ -6830,6 +7995,15 @@ __metadata: languageName: node linkType: hard +"sade@npm:^1.7.3": + version: 1.8.1 + resolution: "sade@npm:1.8.1" + dependencies: + mri: ^1.1.0 + checksum: 0756e5b04c51ccdc8221ebffd1548d0ce5a783a44a0fa9017a026659b97d632913e78f7dca59f2496aa996a0be0b0c322afd87ca72ccd909406f49dbffa0f45d + languageName: node + linkType: hard + "safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -6862,6 +8036,15 @@ __metadata: languageName: node linkType: hard +"scroll-into-view-if-needed@npm:^2.2.20": + version: 2.2.29 + resolution: "scroll-into-view-if-needed@npm:2.2.29" + dependencies: + compute-scroll-into-view: ^1.0.17 + checksum: 6b888404ccf68fe2f2f1da8977e1a8a0a64a7139352e02e98621a0e8be3b3db393519ee3dcfb7c32aff3c4790a36829f1be1cccc0cfb2b90a60a61caa669eee2 + languageName: node + linkType: hard + "sdp-transform@npm:^2.14.1": version: 2.14.1 resolution: "sdp-transform@npm:2.14.1" @@ -6907,6 +8090,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.7": + version: 7.3.7 + resolution: "semver@npm:7.3.7" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 2fa3e877568cd6ce769c75c211beaed1f9fce80b28338cadd9d0b6c40f2e2862bafd62c19a6cff42f3d54292b7c623277bcab8816a2b5521cf15210d43e75232 + languageName: node + linkType: hard + "serialize-javascript@npm:^4.0.0": version: 4.0.0 resolution: "serialize-javascript@npm:4.0.0" @@ -7007,6 +8201,48 @@ __metadata: languageName: node linkType: hard +"slate-history@npm:^0.66.0": + version: 0.66.0 + resolution: "slate-history@npm:0.66.0" + dependencies: + is-plain-object: ^5.0.0 + peerDependencies: + slate: ">=0.65.3" + checksum: e83d4fbf5d4097a5b434df4846420e593598a9a3f90d3e0638f16915b1014bc8a7dc5cd87e3bc0a31a8ad5eae582a18f96bdc04f4b0b0a5a82cf05bd0377601e + languageName: node + linkType: hard + +"slate-react@npm:^0.81.0": + version: 0.81.0 + resolution: "slate-react@npm:0.81.0" + dependencies: + "@types/is-hotkey": ^0.1.1 + "@types/lodash": ^4.14.149 + direction: ^1.0.3 + is-hotkey: ^0.1.6 + is-plain-object: ^5.0.0 + lodash: ^4.17.4 + scroll-into-view-if-needed: ^2.2.20 + tiny-invariant: 1.0.6 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + slate: ">=0.65.3" + checksum: 157272106da0e2af7c370e61f85156b3d3b81baf4bf65f16799ed682ee6c01c49950380b76ac274b2c0c31854e23456043ff18cb42eb3db2f781a3b1ae7be4eb + languageName: node + linkType: hard + +"slate@npm:^0.81.1": + version: 0.81.1 + resolution: "slate@npm:0.81.1" + dependencies: + immer: ^9.0.6 + is-plain-object: ^5.0.0 + tiny-warning: ^1.0.3 + checksum: 550541fa6e95a81a3dfa81d863a67141659f8ac150808b6fd2c71ee5f758390a1d32e227ee6eb67f945cd79b9e6fffbcdc8db6e42b5737ea6427ee379e0afb62 + languageName: node + linkType: hard + "slice-ansi@npm:^4.0.0": version: 4.0.0 resolution: "slice-ansi@npm:4.0.0" @@ -7114,6 +8350,37 @@ __metadata: languageName: node linkType: hard +"space-separated-tokens@npm:^2.0.0": + version: 2.0.1 + resolution: "space-separated-tokens@npm:2.0.1" + checksum: 66e30a6382d6e3ab0a6573d510235a198202071d4ebfef8c198f10433166f0cdced4dbf0946cad3c4b2ecc336896a11f98b2ec93047e140fe7aef6fd3a21365b + languageName: node + linkType: hard + +"spdx-exceptions@npm:^2.1.0": + version: 2.3.0 + resolution: "spdx-exceptions@npm:2.3.0" + checksum: cb69a26fa3b46305637123cd37c85f75610e8c477b6476fa7354eb67c08128d159f1d36715f19be6f9daf4b680337deb8c65acdcae7f2608ba51931540687ac0 + languageName: node + linkType: hard + +"spdx-expression-parse@npm:^3.0.1": + version: 3.0.1 + resolution: "spdx-expression-parse@npm:3.0.1" + dependencies: + spdx-exceptions: ^2.1.0 + spdx-license-ids: ^3.0.0 + checksum: a1c6e104a2cbada7a593eaa9f430bd5e148ef5290d4c0409899855ce8b1c39652bcc88a725259491a82601159d6dc790bedefc9016c7472f7de8de7361f8ccde + languageName: node + linkType: hard + +"spdx-license-ids@npm:^3.0.0": + version: 3.0.11 + resolution: "spdx-license-ids@npm:3.0.11" + checksum: 1da1acb090257773e60b022094050e810ae9fec874dc1461f65dc0400cd42dd830ab2df6e64fb49c2db3dce386dd0362110780e1b154db7c0bb413488836aaeb + languageName: node + linkType: hard + "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -7293,6 +8560,15 @@ __metadata: languageName: node linkType: hard +"style-to-object@npm:^0.3.0": + version: 0.3.0 + resolution: "style-to-object@npm:0.3.0" + dependencies: + inline-style-parser: 0.1.1 + checksum: 4d7084015207f2a606dfc10c29cb5ba569f2fe8005551df7396110dd694d6ff650f2debafa95bd5d147dfb4ca50f57868e2a7f91bf5d11ef734fe7ccbd7abf59 + languageName: node + linkType: hard + "styled-components@npm:^5.3.0": version: 5.3.1 resolution: "styled-components@npm:5.3.1" @@ -7426,13 +8702,27 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:^1.0.2, tiny-invariant@npm:^1.0.6": +"tiny-invariant@npm:1.0.6": + version: 1.0.6 + resolution: "tiny-invariant@npm:1.0.6" + checksum: c90b34beea3cb10c49531e754afb0999033a2d7edffef6602922de27678d8a96dcbc0e8f0a959bc272859281b0cd1305b711e25d5edfb1da5fc7135e2a992961 + languageName: node + linkType: hard + +"tiny-invariant@npm:^1.0.2": version: 1.1.0 resolution: "tiny-invariant@npm:1.1.0" checksum: 27d29bbb9e1d1d86e25766711c28ad91af6d67c87d561167077ac7fbce5212b97bbfe875e70bc369808e075748c825864c9b61f0e9f8652275ec86bcf4dcc924 languageName: node linkType: hard +"tiny-invariant@npm:^1.0.6": + version: 1.2.0 + resolution: "tiny-invariant@npm:1.2.0" + checksum: e09a718a7c4a499ba592cdac61f015d87427a0867ca07f50c11fd9b623f90cdba18937b515d4a5e4f43dac92370498d7bdaee0d0e7a377a61095e02c4a92eade + languageName: node + linkType: hard + "tiny-warning@npm:^1.0.0, tiny-warning@npm:^1.0.3": version: 1.0.3 resolution: "tiny-warning@npm:1.0.3" @@ -7488,6 +8778,20 @@ __metadata: languageName: node linkType: hard +"trim-lines@npm:^3.0.0": + version: 3.0.1 + resolution: "trim-lines@npm:3.0.1" + checksum: e241da104682a0e0d807222cc1496b92e716af4db7a002f4aeff33ae6a0024fef93165d49eab11aa07c71e1347c42d46563f91dfaa4d3fb945aa535cdead53ed + languageName: node + linkType: hard + +"trough@npm:^2.0.0": + version: 2.1.0 + resolution: "trough@npm:2.1.0" + checksum: a577bb561c2b401cc0e1d9e188fcfcdf63b09b151ff56a668da12197fe97cac15e3d77d5b51f426ccfd94255744a9118e9e9935afe81a3644fa1be9783c82886 + languageName: node + linkType: hard + "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -7496,9 +8800,9 @@ __metadata: linkType: hard "tslib@npm:^2.1.0": - version: 2.3.1 - resolution: "tslib@npm:2.3.1" - checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 + version: 2.4.0 + resolution: "tslib@npm:2.4.0" + checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 languageName: node linkType: hard @@ -7576,17 +8880,10 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^0.7.24": - version: 0.7.28 - resolution: "ua-parser-js@npm:0.7.28" - checksum: a7da4ad54527211e878ee016c2ef64efad5c2f5a31277d36c9da93b4c89ecaa64f391ad4cf158ada76a9ad8e53004a950705ff1c2f27a52ca8bfb3f1381c39ff - languageName: node - linkType: hard - -"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5": - version: 1.0.6 - resolution: "uc.micro@npm:1.0.6" - checksum: 6898bb556319a38e9cf175e3628689347bd26fec15fc6b29fa38e0045af63075ff3fea4cf1fdba9db46c9f0cbf07f2348cd8844889dd31ebd288c29fe0d27e7a +"ua-parser-js@npm:^1.0.2": + version: 1.0.2 + resolution: "ua-parser-js@npm:1.0.2" + checksum: ff7f6d79a9c1a38aa85a0e751040fc7e17a0b621bda876838d14ebe55aca4e50e68da0350f181e58801c2d8a35e7db4e12473776e558910c4b7cabcec96aa3bf languageName: node linkType: hard @@ -7649,6 +8946,21 @@ __metadata: languageName: node linkType: hard +"unified@npm:^10.0.0, unified@npm:^10.1.2": + version: 10.1.2 + resolution: "unified@npm:10.1.2" + dependencies: + "@types/unist": ^2.0.0 + bail: ^2.0.0 + extend: ^3.0.0 + is-buffer: ^2.0.0 + is-plain-obj: ^4.0.0 + trough: ^2.0.0 + vfile: ^5.0.0 + checksum: 053e7c65ede644607f87bd625a299e4b709869d2f76ec8138569e6e886903b6988b21cd9699e471eda42bee189527be0a9dac05936f1d069a5e65d0125d5d756 + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -7676,6 +8988,126 @@ __metadata: languageName: node linkType: hard +"unist-builder@npm:^3.0.0": + version: 3.0.0 + resolution: "unist-builder@npm:3.0.0" + dependencies: + "@types/unist": ^2.0.0 + checksum: 80459ee3c2ece90bbc4f4b4faeed524d144c1a09ee07ff3e9004648d9b71a652e80a3b3ef60311a1e92f6ab915caf27c6f08062b5f8c84fa725bc0d7c5759e84 + languageName: node + linkType: hard + +"unist-util-find-after@npm:^4.0.0": + version: 4.0.0 + resolution: "unist-util-find-after@npm:4.0.0" + dependencies: + "@types/unist": ^2.0.0 + unist-util-is: ^5.0.0 + checksum: 8381ef0bad18a0b1fa1c7ee47f94a2578ab6bf572eb126a1f179526b9dca47584fc070976f2d83bbe381161fa33b9164a894d0279a30ec83e65433356d43df57 + languageName: node + linkType: hard + +"unist-util-generated@npm:^2.0.0": + version: 2.0.0 + resolution: "unist-util-generated@npm:2.0.0" + checksum: 3a806793fa24a75190c217740ce706340d6cb0d51eff677134253d628f8e4355ebd8a243fe8045c583463f6bebfd50f902d653161da87c1359fcd1a14b99c8e0 + languageName: node + linkType: hard + +"unist-util-is@npm:^3.0.0": + version: 3.0.0 + resolution: "unist-util-is@npm:3.0.0" + checksum: d24a5dd80c670f763b2ae608651cf062317456aa81be51f66f45cbd7d440a2ab18356e4f48aeac6b5e3d391c69d3c3452ade5fe5aa9574bec4a2de0b10122ed5 + languageName: node + linkType: hard + +"unist-util-is@npm:^4 || ^5, unist-util-is@npm:^5.0.0": + version: 5.1.1 + resolution: "unist-util-is@npm:5.1.1" + checksum: e8743a19a304d8a8f5684f3e5ddb5546f2655847b42123687277d76566a2aba89beb7b4a8a9e9ebc4d904cd1cecc285356d7923d973a43cfc19a1e10ff6bdee4 + languageName: node + linkType: hard + +"unist-util-position@npm:^4.0.0": + version: 4.0.3 + resolution: "unist-util-position@npm:4.0.3" + dependencies: + "@types/unist": ^2.0.0 + checksum: 0d89973628d40f19345cbcc50008f7f56d411afa54434bbe6c224b22d26aaf9d4500da2de363f1f01945acab1f1c31920c514253149eb546ff9b8bbc1ea94209 + languageName: node + linkType: hard + +"unist-util-remove-position@npm:^4.0.0": + version: 4.0.1 + resolution: "unist-util-remove-position@npm:4.0.1" + dependencies: + "@types/unist": ^2.0.0 + unist-util-visit: ^4.0.0 + checksum: 7d2808662ac65f2b2f615822b78060419f738fb3b074b10cec77c596ea966b8f5c47553d2d322822a5975c49d2b21cdd64c198ae9fb02a9d54d1afa6342cdd6a + languageName: node + linkType: hard + +"unist-util-select@npm:^4": + version: 4.0.1 + resolution: "unist-util-select@npm:4.0.1" + dependencies: + "@types/unist": ^2.0.0 + css-selector-parser: ^1.0.0 + nth-check: ^2.0.0 + unist-util-is: ^5.0.0 + zwitch: ^2.0.0 + checksum: da9a69edf03d0c1a633945aae2ddf7153b4d75ff9d51085c55bf742ac0a13df327e6f2925abe863dfb6c451a8865838f28dd029cfb841356b18df56cf8872877 + languageName: node + linkType: hard + +"unist-util-stringify-position@npm:^3.0.0": + version: 3.0.2 + resolution: "unist-util-stringify-position@npm:3.0.2" + dependencies: + "@types/unist": ^2.0.0 + checksum: 2dfd7a0fb2a55e99cc319c3bf7f9f1f73ed652978fa70d19117faa7245d20f21738ec926ecc47f341705ca1bb157e87ced0b6bb5ecaa666bd2ae6b2510d6a671 + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^2.0.0": + version: 2.1.2 + resolution: "unist-util-visit-parents@npm:2.1.2" + dependencies: + unist-util-is: ^3.0.0 + checksum: 048edbb590a8c4bc0043eec9f50d3fe76faa58f1ac663a7e6dee5e895ddd0ce8bc52f2cfe2e633849fa93671e8de021070667acb1518e3d40220768c7f70a3d3 + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^5.0.0": + version: 5.1.0 + resolution: "unist-util-visit-parents@npm:5.1.0" + dependencies: + "@types/unist": ^2.0.0 + unist-util-is: ^5.0.0 + checksum: 7c413dbb3dfcb679109fa8f0965d9abf117c3c53fa7b8823f68cac0ea53adbe98c1ce954d36c034e086c966b48b1d44d42c85f7bf6b42a032f728ac338929513 + languageName: node + linkType: hard + +"unist-util-visit@npm:^1.4.1": + version: 1.4.1 + resolution: "unist-util-visit@npm:1.4.1" + dependencies: + unist-util-visit-parents: ^2.0.0 + checksum: e9395205b6908c8d0fe71bc44e65d89d4781d1bb2d453a33cb67ed4124bad0b89d6b1d526ebaecb82a7c48e211bdf6f24351449b8cc115327b345f4617c18728 + languageName: node + linkType: hard + +"unist-util-visit@npm:^3 || ^4, unist-util-visit@npm:^4.0.0, unist-util-visit@npm:^4.1.0": + version: 4.1.0 + resolution: "unist-util-visit@npm:4.1.0" + dependencies: + "@types/unist": ^2.0.0 + unist-util-is: ^5.0.0 + unist-util-visit-parents: ^5.0.0 + checksum: 3521abee2ed4535092aac073d05f46255475c89781b8e9d8c951a473d91b5d6e4d5912ae4a68a4c1cf17a42ed0108cb93103c7f5c736977529969997451363fb + languageName: node + linkType: hard + "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -7736,6 +9168,20 @@ __metadata: languageName: node linkType: hard +"uvu@npm:^0.5.0": + version: 0.5.6 + resolution: "uvu@npm:0.5.6" + dependencies: + dequal: ^2.0.0 + diff: ^5.0.0 + kleur: ^4.0.3 + sade: ^1.7.3 + bin: + uvu: bin.js + checksum: 09460a37975627de9fcad396e5078fb844d01aaf64a6399ebfcfd9e55f1c2037539b47611e8631f89be07656962af0cf48c334993db82b9ae9c3d25ce3862168 + languageName: node + linkType: hard + "v8-compile-cache@npm:^2.0.3": version: 2.3.0 resolution: "v8-compile-cache@npm:2.3.0" @@ -7750,6 +9196,38 @@ __metadata: languageName: node linkType: hard +"vfile-location@npm:^4.0.0": + version: 4.0.1 + resolution: "vfile-location@npm:4.0.1" + dependencies: + "@types/unist": ^2.0.0 + vfile: ^5.0.0 + checksum: cc0df62075c741beee699e651374aeb56c4c1f4333398c0ba924281c2b51d4b7669c69c5b837ea395775626ad030d6f1bd27fd0a7eaf3f9f1bbd55393948ad6c + languageName: node + linkType: hard + +"vfile-message@npm:^3.0.0": + version: 3.1.2 + resolution: "vfile-message@npm:3.1.2" + dependencies: + "@types/unist": ^2.0.0 + unist-util-stringify-position: ^3.0.0 + checksum: 96fbd9e9b5e0babb5ee61e3a716dc7a6a8c28f2c8c711837d95c88b782161b31549ad16059a78990d7b836d0f4d3b4d8c9ffde44370d48d9cac991fc1e3e17c5 + languageName: node + linkType: hard + +"vfile@npm:^5.0.0": + version: 5.3.4 + resolution: "vfile@npm:5.3.4" + dependencies: + "@types/unist": ^2.0.0 + is-buffer: ^2.0.0 + unist-util-stringify-position: ^3.0.0 + vfile-message: ^3.0.0 + checksum: 2382edc7c6e3502bca72bc95bc1ff0fe1852482e8a0ac257615f9ab12f32564d6f6a55da8756b74a900d26a247da5ca23a92ca7c9a18dbda2b0f87504ef0611f + languageName: node + linkType: hard + "vite-plugin-pwa@npm:^0.11.13": version: 0.11.13 resolution: "vite-plugin-pwa@npm:0.11.13" @@ -7797,6 +9275,13 @@ __metadata: languageName: node linkType: hard +"web-namespaces@npm:^2.0.0": + version: 2.0.1 + resolution: "web-namespaces@npm:2.0.1" + checksum: b6d9f02f1a43d0ef0848a812d89c83801d5bbad57d8bb61f02eb6d7eb794c3736f6cc2e1191664bb26136594c8218ac609f4069722c6f56d9fc2d808fa9271c6 + languageName: node + linkType: hard + "webidl-conversions@npm:^4.0.2": version: 4.0.2 resolution: "webidl-conversions@npm:4.0.2" @@ -8131,3 +9616,10 @@ __metadata: checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"zwitch@npm:^2.0.0": + version: 2.0.2 + resolution: "zwitch@npm:2.0.2" + checksum: 8edd7af8375f12f64d8dbef815af32cd77bd9237d0b013210ba4e3aef25fdc460fe264cd0a19deabe9f86ef0c607240ebac1a336bf4a70bf06ef53e0652de116 + languageName: node + linkType: hard