Merge branch 'master' into production

This commit is contained in:
Paul Makles 2022-05-19 13:42:15 +01:00
commit 4541a34cef
138 changed files with 10981 additions and 6400 deletions

View file

@ -5,3 +5,6 @@ dist_injected
node_modules node_modules
.env .env
.env.local .env.local
Dockerfile
.dockerignore

2
.github/FUNDING.yml vendored
View file

@ -1,2 +0,0 @@
ko_fi: insertish
custom: https://insrt.uk/donate

View file

@ -1,6 +1,6 @@
name: Bug report name: Bug report
description: File a bug report description: File a bug report
title: "[Bug Report]" title: "bug: "
labels: ["bug"] labels: ["bug"]
body: body:
- type: markdown - type: markdown
@ -52,7 +52,7 @@ body:
id: desktop id: desktop
attributes: attributes:
label: Desktop label: Desktop
description: Is this bug specific to [the desktop client](https://gihtub.com/revoltchat/desktop)? (If not, leave this unchecked.) description: Is this bug specific to [the desktop client](https://github.com/revoltchat/desktop)? (If not, leave this unchecked.)
options: options:
- label: Yes, this bug is specific to Revolt Desktop and is *not* an issue with Revolt Desktop itself. - label: Yes, this bug is specific to Revolt Desktop and is *not* an issue with Revolt Desktop itself.
required: false required: false

View file

@ -1,6 +1,6 @@
name: Feature request name: Feature request
description: Make a feature request description: Make a feature request
title: "[Feature Request]" title: "feature request: "
labels: ["enhancement"] labels: ["enhancement"]
body: body:
- type: markdown - type: markdown

24
.github/SECURITY.md vendored
View file

@ -1,24 +0,0 @@
# Security
## Reporting a Vulnerability
If you would like to report a security vulnerability,
please email **[security@revolt.chat](mailto:security@revolt.chat)**,
this will open a new ticket in ticket system, you should receive a response
within the next couple of days, potentially within a few minutes if someone
is currently active.
To help us best triage the issue, please provide:
- The type of issue at hand
- The name of the relevant project affected
- Reproduction steps
- Reference to any relevant source file(s) that you may suspect are causing the issue
- Any extra information about your configuration.
- Description of potential ways this can be exploited, if you can list any
For revoltchat/revite in particular:
- Please include the commit hash of the client, it is visible in settings under the log out button.
Thank you for helping Revolt.

3
.gitignore vendored
View file

@ -7,6 +7,9 @@ dist-ssr
*.log *.log
/.idea /.idea
.yarn/cache
.yarn/install-state.gz
public/assets public/assets
public/assets_* public/assets_*
!public/assets_default !public/assets_default

View file

@ -1,5 +1,4 @@
{ {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true
"compile-hero.disable-compile-files-on-did-save-code": true
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

785
.yarn/releases/yarn-3.2.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

11
.yarnrc.yml Normal file
View file

@ -0,0 +1,11 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.2.0.cjs

View file

@ -1,18 +1,15 @@
FROM node:16-buster AS builder FROM node:16-buster AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn --no-cache
COPY . . COPY . .
COPY .env.build .env COPY .env.build .env
RUN yarn add --dev @babel/plugin-proposal-decorators
RUN yarn typecheck
RUN yarn build
RUN npm prune --production
FROM node:16-buster RUN yarn install --frozen-lockfile
RUN yarn typecheck
RUN yarn build:highmem
RUN yarn workspaces focus --production --all
FROM node:16-alpine
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=builder /usr/src/app . COPY --from=builder /usr/src/app .

2
external/lang vendored

@ -1 +1 @@
Subproject commit 6a72c5c952eedfbeb8a193a8a4b97927cc44cd6f Subproject commit bac88cffd196a2afacf7d726e4f7ef19bd6bd94c

View file

@ -4,6 +4,7 @@
"dev": "node scripts/setup_assets.js --check && vite", "dev": "node scripts/setup_assets.js --check && vite",
"pull": "node scripts/setup_assets.js", "pull": "node scripts/setup_assets.js",
"build": "rimraf build && node scripts/setup_assets.js --check && vite build", "build": "rimraf build && node scripts/setup_assets.js --check && vite build",
"build:highmem": "NODE_OPTIONS='--max-old-space-size=4096' yarn build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}", "lint": "eslint src/**/*.{js,jsx,ts,tsx}",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
@ -61,7 +62,6 @@
"dependencies": { "dependencies": {
"@fontsource/bitter": "^4.5.0", "@fontsource/bitter": "^4.5.0",
"@insertish/vite-plugin-babel-macros": "^1.0.5", "@insertish/vite-plugin-babel-macros": "^1.0.5",
"color-rgba": "^2.3.0",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"klaw": "^3.0.0", "klaw": "^3.0.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
@ -69,7 +69,7 @@
"vite": "^2.6.14" "vite": "^2.6.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-decorators": "^7.16.7", "@babel/plugin-proposal-decorators": "^7.17.9",
"@fontsource/atkinson-hyperlegible": "^4.4.5", "@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/comic-neue": "^4.4.5", "@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5", "@fontsource/fira-code": "^4.4.5",
@ -81,7 +81,7 @@
"@fontsource/noto-sans": "^4.4.5", "@fontsource/noto-sans": "^4.4.5",
"@fontsource/open-sans": "^4.5.2", "@fontsource/open-sans": "^4.5.2",
"@fontsource/opendyslexic": "^4.5.2", "@fontsource/opendyslexic": "^4.5.2",
"@fontsource/poppins": "^4.4.5", "@fontsource/poppins": "^4.4.5",
"@fontsource/raleway": "^4.4.5", "@fontsource/raleway": "^4.4.5",
"@fontsource/roboto": "^4.4.5", "@fontsource/roboto": "^4.4.5",
"@fontsource/roboto-mono": "^4.4.5", "@fontsource/roboto-mono": "^4.4.5",
@ -91,16 +91,16 @@
"@fontsource/ubuntu-mono": "^4.4.5", "@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6", "@hcaptcha/react-hcaptcha": "^0.3.6",
"@preact/preset-vite": "^2.0.0", "@preact/preset-vite": "^2.0.0",
"@revoltchat/ui": "1.0.31",
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.34.0", "@styled-icons/boxicons-logos": "^10.38.0",
"@styled-icons/boxicons-regular": "^10.34.0", "@styled-icons/boxicons-regular": "^10.38.0",
"@styled-icons/boxicons-solid": "^10.37.0", "@styled-icons/boxicons-solid": "^10.38.0",
"@styled-icons/simple-icons": "^10.33.0", "@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "^4.2.5", "@tippyjs/react": "^4.2.5",
"@traptitech/markdown-it-katex": "^3.4.3", "@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6", "@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2", "@trivago/prettier-plugin-sort-imports": "^2.0.2",
"@types/color-rgba": "^2.1.0",
"@types/lodash.defaultsdeep": "^4.6.6", "@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.isequal": "^4.5.5", "@types/lodash.isequal": "^4.5.5",
"@types/markdown-it": "^12.0.2", "@types/markdown-it": "^12.0.2",
@ -111,13 +111,13 @@
"@types/react-helmet": "^6.1.1", "@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2", "@types/react-scroll": "^1.8.2",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/styled-components": "^5.1.10", "@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1", "@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0", "@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.27.0", "@typescript-eslint/parser": "^4.27.0",
"@vitejs/plugin-legacy": "^1.7.1", "@vitejs/plugin-legacy": "^1.7.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"color-rgba": "^2.4.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"detect-browser": "^5.2.0", "detect-browser": "^5.2.0",
"eslint": "^7.28.0", "eslint": "^7.28.0",
@ -127,13 +127,12 @@
"localforage": "^1.9.0", "localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1", "lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"long": "^5.2.0",
"markdown-it": "^12.0.6", "markdown-it": "^12.0.6",
"markdown-it-emoji": "^2.0.0", "markdown-it-emoji": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.3.2", "mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0", "mobx-react-lite": "^3.3.0",
"preact": "^10.5.14", "preact": "^10.5.14",
"preact-context-menu": "0.4.0-patch.0", "preact-context-menu": "0.4.0-patch.0",
"preact-i18n": "^2.4.0-preactx", "preact-i18n": "^2.4.0-preactx",
@ -145,10 +144,8 @@
"react-overlapping-panels": "1.2.2", "react-overlapping-panels": "1.2.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2", "react-scroll": "^1.8.2",
"react-virtualized-auto-sizer": "^1.0.5",
"react-virtuoso": "^1.10.4", "react-virtuoso": "^1.10.4",
"revolt-api": "^0.5.3-alpha.12", "revolt.js": "6.0.0-2",
"revolt.js": "^5.2.8",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.35.1", "sass": "^1.35.1",
"shade-blend-color": "^1.0.0", "shade-blend-color": "^1.0.0",
@ -160,10 +157,10 @@
"vite-plugin-pwa": "^0.11.13", "vite-plugin-pwa": "^0.11.13",
"workbox-precaching": "^6.1.5" "workbox-precaching": "^6.1.5"
}, },
"packageManager": "yarn@1.22.17",
"name": "client", "name": "client",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/revoltchat/revite.git", "repository": "https://github.com/revoltchat/revite.git",
"author": "Paul <paulmakles@gmail.com>", "author": "Paul <paulmakles@gmail.com>",
"license": "MIT" "license": "MIT",
} "packageManager": "yarn@3.2.0"
}

View file

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

After

Width:  |  Height:  |  Size: 626 B

View file

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

After

Width:  |  Height:  |  Size: 626 B

View file

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

After

Width:  |  Height:  |  Size: 626 B

View file

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

After

Width:  |  Height:  |  Size: 626 B

View file

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

After

Width:  |  Height:  |  Size: 626 B

View file

@ -0,0 +1 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

After

Width:  |  Height:  |  Size: 626 B

View file

@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View file

@ -1,5 +1,4 @@
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel, User } from "revolt.js";
import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { StateUpdater, useState } from "preact/hooks"; import { StateUpdater, useState } from "preact/hooks";

View file

@ -1,14 +1,15 @@
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular"; import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
import { ImageIconBase, IconBaseProps } from "./IconBase";
import fallback from "./assets/group.png"; import fallback from "./assets/group.png";
import { ImageIconBase, IconBaseProps } from "./IconBase";
interface Props extends IconBaseProps<Channel> { interface Props extends IconBaseProps<Channel> {
isServerChannel?: boolean; isServerChannel?: boolean;
} }
@ -32,7 +33,7 @@ export default observer(
...imgProps ...imgProps
} = props; } = props;
const iconURL = client.generateFileURL( const iconURL = client.generateFileURL(
target?.icon ?? attachment, target?.icon ?? attachment ?? undefined,
{ max_side: 256 }, { max_side: 256 },
animate, animate,
); );

View file

@ -1,4 +1,5 @@
import { Attachment } from "revolt-api/types/Autumn"; import { API } from "revolt.js";
import { Nullable } from "revolt.js";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Ref } from "preact"; import { Ref } from "preact";
@ -6,7 +7,7 @@ import { Ref } from "preact";
export interface IconBaseProps<T> { export interface IconBaseProps<T> {
target?: T; target?: T;
url?: string; url?: string;
attachment?: Attachment; attachment?: Nullable<API.File>;
size: number; size: number;
hover?: boolean; hover?: boolean;

View file

@ -2,14 +2,11 @@ import { Check } from "@styled-icons/boxicons-regular";
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { ServerPermission } from "revolt.js/dist/api/permissions"; import { Server } from "revolt.js";
import { Server } from "revolt.js/dist/maps/Servers";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
@ -125,7 +122,7 @@ export default observer(({ server }: Props) => {
</Tooltip> </Tooltip>
) : undefined} ) : undefined}
<div className="title">{server.name}</div> <div className="title">{server.name}</div>
{(server.permission & ServerPermission.ManageServer) > 0 && ( {server.havePermission("ManageServer") && (
<Link to={`/server/${server._id}/settings`}> <Link to={`/server/${server._id}/settings`}>
<IconButton> <IconButton>
<Cog size={20} /> <Cog size={20} />

View file

@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
@ -39,7 +39,7 @@ export default observer(
const { target, attachment, size, animate, server_name, ...imgProps } = const { target, attachment, size, animate, server_name, ...imgProps } =
props; props;
const iconURL = client.generateFileURL( const iconURL = client.generateFileURL(
target?.icon ?? attachment, target?.icon ?? attachment ?? undefined,
{ max_side: 256 }, { max_side: 256 },
animate, animate,
); );

View file

@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Message as MessageObject } from "revolt.js/dist/maps/Messages"; import { Message as MessageObject } from "revolt.js";
import { useTriggerEvents } from "preact-context-menu"; import { useTriggerEvents } from "preact-context-menu";
import { memo } from "preact/compat"; import { memo } from "preact/compat";

View file

@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js";
import styled, { css, keyframes } from "styled-components/macro"; import styled, { css, keyframes } from "styled-components/macro";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";

View file

@ -1,8 +1,16 @@
import { Send, ShieldX, HappyBeaming, Box } from "@styled-icons/boxicons-solid"; import { Send, ShieldX } from "@styled-icons/boxicons-solid";
import Axios, { CancelTokenSource } from "axios"; import Axios, { CancelTokenSource } from "axios";
import Long from "long";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ChannelPermission } from "revolt.js/dist/api/permissions"; import {
import { Channel } from "revolt.js/dist/maps/Channels"; Channel,
DEFAULT_PERMISSION_DIRECT_MESSAGE,
DEFAULT_PERMISSION_VIEW_ONLY,
Permission,
Server,
U32_MAX,
UserPermission,
} from "revolt.js";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { ulid } from "ulid"; import { ulid } from "ulid";
@ -125,6 +133,11 @@ const FileAction = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
}
`;
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
width: 16px;
`; `;
// For sed replacement // For sed replacement
@ -150,7 +163,7 @@ export default observer(({ channel }: Props) => {
const renderer = getRenderer(channel); const renderer = getRenderer(channel);
if (!(channel.permission & ChannelPermission.SendMessage)) { if (!channel.havePermission("SendMessage")) {
return ( return (
<Base> <Base>
<Blocked> <Blocked>
@ -231,7 +244,7 @@ export default observer(({ channel }: Props) => {
); );
renderer.messages.reverse(); renderer.messages.reverse();
if (msg) { if (msg?.content) {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [_, toReplace, newText, flags] = content.split(/\//); let [_, toReplace, newText, flags] = content.split(/\//);
@ -493,7 +506,7 @@ export default observer(({ channel }: Props) => {
setReplies={setReplies} setReplies={setReplies}
/> />
<Base> <Base>
{channel.permission & ChannelPermission.UploadFiles ? ( {channel.havePermission("UploadFiles") ? (
<FileAction> <FileAction>
<FileUploader <FileUploader
size={24} size={24}
@ -530,7 +543,9 @@ export default observer(({ channel }: Props) => {
}} }}
/> />
</FileAction> </FileAction>
) : undefined} ) : (
<ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding />
)}
<TextAreaAutoSize <TextAreaAutoSize
autoFocus autoFocus
hideBorder hideBorder

View file

@ -11,8 +11,7 @@ import {
MessageSquareEdit, MessageSquareEdit,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { SystemMessage as SystemMessageI } from "revolt-api/types/Channels"; import { Message, API } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { useTriggerEvents } from "preact-context-menu"; import { useTriggerEvents } from "preact-context-menu";
@ -75,13 +74,11 @@ export const SystemMessage = observer(
({ attachContext, message, highlight, hideInfo }: Props) => { ({ attachContext, message, highlight, hideInfo }: Props) => {
const data = message.asSystemMessage; const data = message.asSystemMessage;
const SystemMessageIcon = const SystemMessageIcon =
iconDictionary[data.type as SystemMessageI["type"]] ?? InfoCircle; iconDictionary[data.type as API.SystemMessage["type"]] ??
InfoCircle;
let children; let children = null;
switch (data.type) { switch (data.type) {
case "text":
children = <span>{data.content}</span>;
break;
case "user_added": case "user_added":
case "user_remove": case "user_remove":
children = ( children = (

View file

@ -1,4 +1,4 @@
import { Attachment as AttachmentI } from "revolt-api/types/Autumn"; import { API } from "revolt.js";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@ -14,7 +14,7 @@ import Spoiler from "./Spoiler";
import TextFile from "./TextFile"; import TextFile from "./TextFile";
interface Props { interface Props {
attachment: AttachmentI; attachment: API.File;
hasContent?: boolean; hasContent?: boolean;
} }

View file

@ -4,7 +4,8 @@ import {
Download, Download,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { File, Video } from "@styled-icons/boxicons-solid"; import { File, Video } from "@styled-icons/boxicons-solid";
import { Attachment } from "revolt-api/types/Autumn"; import { isFirefox } from "react-device-detect";
import { API } from "revolt.js";
import styles from "./AttachmentActions.module.scss"; import styles from "./AttachmentActions.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@ -17,7 +18,7 @@ import { AppContext } from "../../../../context/revoltjs/RevoltClient";
import IconButton from "../../../ui/IconButton"; import IconButton from "../../../ui/IconButton";
interface Props { interface Props {
attachment: Attachment; attachment: API.File;
} }
export default function AttachmentActions({ attachment }: Props) { export default function AttachmentActions({ attachment }: Props) {
@ -51,7 +52,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target="_blank" target={isFirefox || window.native ? "_blank" : "_self"}
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />
@ -69,7 +70,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target="_blank" target={isFirefox || window.native ? "_blank" : "_self"}
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />
@ -89,7 +90,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target="_blank" target={isFirefox || window.native ? "_blank" : "_self"}
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />
@ -118,7 +119,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target="_blank" target={isFirefox || window.native ? "_blank" : "_self"}
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />

View file

@ -9,6 +9,7 @@ const Grid = styled.div<{ width: number; height: number }>`
--height: ${(props) => props.height}px; --height: ${(props) => props.height}px;
display: grid; display: grid;
overflow: hidden;
aspect-ratio: ${(props) => props.width} / ${(props) => props.height}; aspect-ratio: ${(props) => props.width} / ${(props) => props.height};
max-width: min(var(--width), var(--attachment-max-width)); max-width: min(var(--width), var(--attachment-max-width));

View file

@ -1,4 +1,4 @@
import { Attachment } from "revolt-api/types/Autumn"; import { API } from "revolt.js";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@ -10,12 +10,12 @@ import { AppContext } from "../../../../context/revoltjs/RevoltClient";
enum ImageLoadingState { enum ImageLoadingState {
Loading, Loading,
Loaded, Loaded,
Error Error,
} }
type Props = JSX.HTMLAttributes<HTMLImageElement> & { type Props = JSX.HTMLAttributes<HTMLImageElement> & {
attachment: Attachment; attachment: API.File;
} };
export default function ImageFile({ attachment, ...props }: Props) { export default function ImageFile({ attachment, ...props }: Props) {
const [loading, setLoading] = useState(ImageLoadingState.Loading); const [loading, setLoading] = useState(ImageLoadingState.Loading);
@ -23,25 +23,19 @@ export default function ImageFile({ attachment, ...props }: Props) {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const url = client.generateFileURL(attachment)!; const url = client.generateFileURL(attachment)!;
return <img return (
{...props} <img
src={url} {...props}
alt={attachment.filename} src={url}
loading="lazy" alt={attachment.filename}
className={classNames(styles.image, { loading="lazy"
[styles.loading]: loading !== ImageLoadingState.Loaded className={classNames(styles.image, {
})} [styles.loading]: loading !== ImageLoadingState.Loaded,
onClick={() => })}
openScreen({ id: "image_viewer", attachment }) onClick={() => openScreen({ id: "image_viewer", attachment })}
} onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
onMouseDown={(ev) => onLoad={() => setLoading(ImageLoadingState.Loaded)}
ev.button === 1 && window.open(url, "_blank") onError={() => setLoading(ImageLoadingState.Error)}
} />
onLoad={() => );
setLoading(ImageLoadingState.Loaded)
}
onError={() =>
setLoading(ImageLoadingState.Error)
}
/>
} }

View file

@ -2,9 +2,7 @@ import { Reply } from "@styled-icons/boxicons-regular";
import { File } from "@styled-icons/boxicons-solid"; import { File } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users"; import { Channel, Message, API } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -174,7 +172,7 @@ export const MessageReply = observer(
<ReplyBase head={index === 0}> <ReplyBase head={index === 0}>
{/*<Reply size={16} />*/} {/*<Reply size={16} />*/}
{message.author?.relationship === RelationshipStatus.Blocked ? ( {message.author?.relationship === "Blocked" ? (
<Text id="app.main.channel.misc.blocked_user" /> <Text id="app.main.channel.misc.blocked_user" />
) : ( ) : (
<> <>
@ -225,9 +223,10 @@ export const MessageReply = observer(
)} )}
<Markdown <Markdown
disallowBigEmoji disallowBigEmoji
content={( content={message.content?.replace(
message.content as string /\n/g,
).replace(/\n/g, " ")} " ",
)}
/> />
</div> </div>
</> </>

View file

@ -1,7 +1,8 @@
import axios from "axios"; import axios from "axios";
import { Attachment } from "revolt-api/types/Autumn"; import { API } from "revolt.js";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline"; import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
@ -11,14 +12,16 @@ import {
} from "../../../../context/revoltjs/RevoltClient"; } from "../../../../context/revoltjs/RevoltClient";
import Preloader from "../../../ui/Preloader"; import Preloader from "../../../ui/Preloader";
import { Button } from "@revoltchat/ui";
interface Props { interface Props {
attachment: Attachment; attachment: API.File;
} }
const fileCache: { [key: string]: string } = {}; const fileCache: { [key: string]: string } = {};
export default function TextFile({ attachment }: Props) { export default function TextFile({ attachment }: Props) {
const [gated, setGated] = useState(attachment.size > 100_000);
const [content, setContent] = useState<undefined | string>(undefined); const [content, setContent] = useState<undefined | string>(undefined);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const status = useContext(StatusContext); const status = useContext(StatusContext);
@ -29,13 +32,7 @@ export default function TextFile({ attachment }: Props) {
useEffect(() => { useEffect(() => {
if (typeof content !== "undefined") return; if (typeof content !== "undefined") return;
if (loading) return; if (loading) return;
if (gated) return;
if (attachment.size > 100_000) {
setContent(
"This file is > 100 KB, for your sake I did not load it.\nSee tracking issue here for previews: https://github.com/revoltchat/revite/issues/35",
);
return;
}
setLoading(true); setLoading(true);
@ -60,13 +57,17 @@ export default function TextFile({ attachment }: Props) {
setLoading(false); setLoading(false);
}); });
} }
}, [content, loading, status, attachment._id, attachment.size, url]); }, [content, loading, gated, status, attachment._id, attachment.size, url]);
return ( return (
<div <div
className={styles.textContent} className={styles.textContent}
data-loading={typeof content === "undefined"}> data-loading={typeof content === "undefined"}>
{content ? ( {gated ? (
<Button palette="accent" onClick={() => setGated(false)}>
<Text id="app.main.channel.misc.load_file" />
</Button>
) : content ? (
<pre> <pre>
<code>{content}</code> <code>{content}</code>
</pre> </pre>

View file

@ -1,6 +1,6 @@
import { DownArrowAlt } from "@styled-icons/boxicons-regular"; import { DownArrowAlt } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View file

@ -7,8 +7,8 @@ import {
Notification, Notification,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ChannelPermission } from "revolt.js"; import { Permission } from "revolt.js";
import { Message as MessageObject } from "revolt.js/dist/maps/Messages"; import { Message as MessageObject } from "revolt.js";
import styled from "styled-components"; import styled from "styled-components";
import { openContextMenu } from "preact-context-menu"; import { openContextMenu } from "preact-context-menu";
@ -131,8 +131,7 @@ export const MessageOverlayBar = observer(({ message, queued }: Props) => {
)} )}
{isAuthor || {isAuthor ||
(message.channel && (message.channel &&
message.channel.permission & message.channel.havePermission("ManageMessages")) ? (
ChannelPermission.ManageMessages) ? (
<Tooltip content="Delete"> <Tooltip content="Delete">
<Entry <Entry
onClick={(e) => onClick={(e) =>

View file

@ -1,7 +1,7 @@
import { UpArrowAlt } from "@styled-icons/boxicons-regular"; import { UpArrowAlt } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View file

@ -1,8 +1,7 @@
import { At, Reply as ReplyIcon } from "@styled-icons/boxicons-regular"; import { At } from "@styled-icons/boxicons-regular";
import { File, XCircle } from "@styled-icons/boxicons-solid"; import { File, XCircle } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel, Message } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -188,9 +187,10 @@ export default observer(({ channel, replies, setReplies }: Props) => {
) : ( ) : (
<Markdown <Markdown
disallowBigEmoji disallowBigEmoji
content={( content={message.content?.replace(
message.content as string /\n/g,
).replace(/\n/g, " ")} " ",
)}
/> />
)} )}
</div> </div>

View file

@ -1,6 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { RelationshipStatus } from "revolt-api/types/Users"; import { Channel } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -65,7 +64,7 @@ export default observer(({ channel }: Props) => {
(x) => (x) =>
typeof x !== "undefined" && typeof x !== "undefined" &&
x._id !== x.client.user!._id && x._id !== x.client.user!._id &&
x.relationship !== RelationshipStatus.Blocked, x.relationship !== "Blocked",
); );
if (users.length > 0) { if (users.length > 0) {

View file

@ -1,4 +1,4 @@
import { Embed as EmbedI } from "revolt-api/types/Channels"; import { API } from "revolt.js";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@ -13,7 +13,7 @@ import Attachment from "../attachments/Attachment";
import EmbedMedia from "./EmbedMedia"; import EmbedMedia from "./EmbedMedia";
interface Props { interface Props {
embed: EmbedI; embed: API.Embed;
} }
const MAX_EMBED_WIDTH = 480; const MAX_EMBED_WIDTH = 480;
@ -68,7 +68,8 @@ export default function Embed({ embed }: Props) {
mh = embed.video?.height ?? 720; mh = embed.video?.height ?? 720;
break; break;
} }
case "Twitch": { case "Twitch":
case "Lightspeed": {
mw = 1280; mw = 1280;
mh = 720; mh = 720;
break; break;
@ -89,6 +90,20 @@ export default function Embed({ embed }: Props) {
} }
const { width, height } = calculateSize(mw, mh); const { width, height } = calculateSize(mw, mh);
if (embed.type === "Website" && embed.special?.type === "GIF") {
return (
<EmbedMedia
embed={embed}
width={
height *
((embed.image?.width ?? 0) /
(embed.image?.height ?? 0))
}
height={height}
/>
);
}
return ( return (
<div <div
className={classNames(styles.embed, styles.website)} className={classNames(styles.embed, styles.website)}
@ -128,7 +143,7 @@ export default function Embed({ embed }: Props) {
<a <a
onMouseDown={(ev) => onMouseDown={(ev) =>
(ev.button === 0 || ev.button === 1) && (ev.button === 0 || ev.button === 1) &&
openLink(embed.url) openLink(embed.url!)
} }
className={styles.title}> className={styles.title}>
{embed.title} {embed.title}
@ -181,6 +196,18 @@ export default function Embed({ embed }: Props) {
/> />
); );
} }
case "Video": {
return (
<video
className={classNames(styles.embed, styles.image)}
style={calculateSize(embed.width, embed.height)}
src={client.proxyFile(embed.url)}
frameBorder="0"
loading="lazy"
controls
/>
);
}
default: default:
return null; return null;
} }

View file

@ -1,14 +1,12 @@
import { Group } from "@styled-icons/boxicons-solid"; import { Group } from "@styled-icons/boxicons-solid";
import { autorun, reaction } from "mobx"; import { reaction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { RetrievedInvite } from "revolt-api/types/Invites"; import { Message, API } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { defer } from "../../../../lib/defer";
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
import { import {
@ -85,9 +83,9 @@ export function EmbedInvite({ code }: Props) {
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const [joinError, setJoinError] = useState<string | undefined>(undefined); const [joinError, setJoinError] = useState<string | undefined>(undefined);
const [invite, setInvite] = useState<RetrievedInvite | undefined>( const [invite, setInvite] = useState<
undefined, (API.InviteResponse & { type: "Server" }) | undefined
); >(undefined);
useEffect(() => { useEffect(() => {
if ( if (
@ -96,7 +94,9 @@ export function EmbedInvite({ code }: Props) {
) { ) {
client client
.fetchInvite(code) .fetchInvite(code)
.then((data) => setInvite(data)) .then((data) =>
setInvite(data as API.InviteResponse & { type: "Server" }),
)
.catch((err) => setError(takeError(err))); .catch((err) => setError(takeError(err)));
} }
}, [client, code, invite, status]); }, [client, code, invite, status]);
@ -139,42 +139,17 @@ export function EmbedInvite({ code }: Props) {
) : ( ) : (
<Button <Button
onClick={async () => { onClick={async () => {
setProcessing(true);
try { try {
setProcessing(true); await client.joinInvite(invite);
if (invite.type === "Server") { history.push(
if (client.servers.get(invite.server_id)) { `/server/${invite.server_id}/channel/${invite.channel_id}`,
history.push( );
`/server/${invite.server_id}/channel/${invite.channel_id}`,
);
return;
}
const dispose = reaction(
() =>
client.servers.get(
invite.server_id,
),
(server) => {
if (server) {
client.unreads!.markMultipleRead(
server.channel_ids,
);
history.push(
`/server/${server._id}/channel/${invite.channel_id}`,
);
dispose();
}
},
);
}
await client.joinInvite(code);
} catch (err) { } catch (err) {
setJoinError(takeError(err)); setJoinError(takeError(err));
} finally {
setProcessing(false); setProcessing(false);
} }
}}> }}>

View file

@ -1,5 +1,5 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { JanuaryEmbed } from "revolt-api/types/January"; import { API } from "revolt.js";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
@ -7,7 +7,7 @@ import { useIntermediate } from "../../../../context/intermediate/Intermediate";
import { useClient } from "../../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../../context/revoltjs/RevoltClient";
interface Props { interface Props {
embed: JanuaryEmbed; embed: API.Embed;
width?: number; width?: number;
height: number; height: number;
} }
@ -47,6 +47,17 @@ export default function EmbedMedia({ embed, width, height }: Props) {
style={{ height }} style={{ height }}
/> />
); );
case "Lightspeed":
return (
<iframe
src={`https://next.lightspeed.tv/embed/${embed.special.id}`}
frameBorder="0"
allowFullScreen
scrolling="no"
loading="lazy"
style={{ height }}
/>
);
case "Spotify": case "Spotify":
return ( return (
<iframe <iframe
@ -83,7 +94,21 @@ export default function EmbedMedia({ embed, width, height }: Props) {
); );
} }
default: { default: {
if (embed.image) { if (embed.video) {
const url = embed.video.url;
return (
<video
loading="lazy"
className={styles.image}
style={{ width, height }}
src={client.proxyFile(url)}
loop={embed.special?.type === "GIF"}
controls={embed.special?.type !== "GIF"}
autoPlay={embed.special?.type === "GIF"}
muted={embed.special?.type === "GIF" ? true : undefined}
/>
);
} else if (embed.image) {
const url = embed.image.url; const url = embed.image.url;
return ( return (
<img <img
@ -94,7 +119,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "image_viewer", id: "image_viewer",
embed: embed.image, embed: embed.image!,
}) })
} }
onMouseDown={(ev) => onMouseDown={(ev) =>

View file

@ -1,12 +1,12 @@
import { LinkExternal } from "@styled-icons/boxicons-regular"; import { LinkExternal } from "@styled-icons/boxicons-regular";
import { EmbedImage } from "revolt-api/types/January"; import { API } from "revolt.js";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
import IconButton from "../../../ui/IconButton"; import IconButton from "../../../ui/IconButton";
interface Props { interface Props {
embed: EmbedImage; embed: API.Image;
} }
export default function EmbedMediaActions({ embed }: Props) { export default function EmbedMediaActions({ embed }: Props) {

View file

@ -1,11 +1,23 @@
import { Shield } from "@styled-icons/boxicons-regular"; import { Shield } from "@styled-icons/boxicons-regular";
import { Badges } from "revolt-api/types/Users";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Localizer, Text } from "preact-i18n"; import { Localizer, Text } from "preact-i18n";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
enum Badges {
Developer = 1,
Translator = 2,
Supporter = 4,
ResponsibleDisclosure = 8,
Founder = 16,
PlatformModeration = 32,
ActiveSupporter = 64,
Paw = 128,
EarlyAdopter = 256,
ReservedRelevantJokeBadge1 = 512,
}
const BadgesBase = styled.div` const BadgesBase = styled.div`
gap: 8px; gap: 8px;
display: flex; display: flex;

View file

@ -1,4 +1,4 @@
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js";
import Checkbox, { CheckboxProps } from "../../ui/Checkbox"; import Checkbox, { CheckboxProps } from "../../ui/Checkbox";

View file

@ -1,7 +1,7 @@
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { openContextMenu } from "preact-context-menu"; import { openContextMenu } from "preact-context-menu";

View file

@ -1,4 +1,4 @@
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";

View file

@ -1,9 +1,7 @@
import { VolumeMute, MicrophoneOff } from "@styled-icons/boxicons-solid"; import { VolumeMute, MicrophoneOff } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Masquerade } from "revolt-api/types/Channels"; import { User, API } from "revolt.js";
import { Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
@ -18,17 +16,17 @@ type VoiceStatus = "muted" | "deaf";
interface Props extends IconBaseProps<User> { interface Props extends IconBaseProps<User> {
status?: boolean; status?: boolean;
voice?: VoiceStatus; voice?: VoiceStatus;
masquerade?: Masquerade; masquerade?: API.Masquerade;
showServerIdentity?: boolean; showServerIdentity?: boolean;
} }
export function useStatusColour(user?: User) { export function useStatusColour(user?: User) {
const theme = useApplicationState().settings.theme; const theme = useApplicationState().settings.theme;
return user?.online && user?.status?.presence !== Presence.Invisible return user?.online && user?.status?.presence !== "Invisible"
? user?.status?.presence === Presence.Idle ? user?.status?.presence === "Idle"
? theme.getVariable("status-away") ? theme.getVariable("status-away")
: user?.status?.presence === Presence.Busy : user?.status?.presence === "Busy"
? theme.getVariable("status-busy") ? theme.getVariable("status-busy")
: theme.getVariable("status-online") : theme.getVariable("status-online")
: theme.getVariable("status-invisible"); : theme.getVariable("status-invisible");
@ -76,7 +74,7 @@ export default observer(
let { url } = props; let { url } = props;
if (masquerade?.avatar) { if (masquerade?.avatar) {
url = masquerade.avatar; url = client.proxyFile(masquerade.avatar);
} else if (!url) { } else if (!url) {
let override; let override;
if (target && showServerIdentity) { if (target && showServerIdentity) {
@ -95,7 +93,7 @@ export default observer(
url = url =
client.generateFileURL( client.generateFileURL(
override ?? target?.avatar ?? attachment, override ?? target?.avatar ?? attachment ?? undefined,
{ max_side: 256 }, { max_side: 256 },
animate, animate,
) ?? (target ? target.defaultAvatarURL : fallback); ) ?? (target ? target.defaultAvatarURL : fallback);

View file

@ -1,8 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Masquerade } from "revolt-api/types/Channels"; import { User, API } from "revolt.js";
import { User } from "revolt.js/dist/maps/Users";
import { Nullable } from "revolt.js/dist/util/null";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Ref } from "preact"; import { Ref } from "preact";
@ -32,7 +30,7 @@ const BotBadge = styled.div`
type UsernameProps = JSX.HTMLAttributes<HTMLElement> & { type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
user?: User; user?: User;
prefixAt?: boolean; prefixAt?: boolean;
masquerade?: Masquerade; masquerade?: API.Masquerade;
showServerIdentity?: boolean | "both"; showServerIdentity?: boolean | "both";
innerRef?: Ref<any>; innerRef?: Ref<any>;
@ -93,7 +91,11 @@ export const Username = observer(
)} )}
</span> </span>
<BotBadge> <BotBadge>
<Text id="app.main.channel.bot" /> {masquerade ? (
<Text id="app.main.channel.bridge" />
) : (
<Text id="app.main.channel.bot" />
)}
</BotBadge> </BotBadge>
</> </>
); );
@ -120,7 +122,7 @@ export default function UserShort({
user?: User; user?: User;
size?: number; size?: number;
prefixAt?: boolean; prefixAt?: boolean;
masquerade?: Masquerade; masquerade?: API.Masquerade;
showServerIdentity?: boolean; showServerIdentity?: boolean;
}) { }) {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();

View file

@ -1,6 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Presence } from "revolt-api/types/Users"; import { User, API } from "revolt.js";
import { User } from "revolt.js/dist/maps/Users";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -25,15 +24,15 @@ export default observer(({ user, tooltip }: Props) => {
return <>{user.status.text}</>; return <>{user.status.text}</>;
} }
if (user.status?.presence === Presence.Busy) { if (user.status?.presence === "Busy") {
return <Text id="app.status.busy" />; return <Text id="app.status.busy" />;
} }
if (user.status?.presence === Presence.Idle) { if (user.status?.presence === "Idle") {
return <Text id="app.status.idle" />; return <Text id="app.status.idle" />;
} }
if (user.status?.presence === Presence.Invisible) { if (user.status?.presence === "Invisible") {
return <Text id="app.status.offline" />; return <Text id="app.status.offline" />;
} }

View file

@ -3,7 +3,7 @@ import { Suspense, lazy } from "preact/compat";
const Renderer = lazy(() => import("./Renderer")); const Renderer = lazy(() => import("./Renderer"));
export interface MarkdownProps { export interface MarkdownProps {
content?: string; content?: string | null;
disallowBigEmoji?: boolean; disallowBigEmoji?: boolean;
} }

View file

@ -5,10 +5,6 @@ import "katex/dist/katex.min.css";
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
// @ts-expect-error No typings. // @ts-expect-error No typings.
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare"; import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
// @ts-expect-error No typings.
import MarkdownSub from "markdown-it-sub";
// @ts-expect-error No typings.
import MarkdownSup from "markdown-it-sup";
import { RE_MENTIONS } from "revolt.js"; import { RE_MENTIONS } from "revolt.js";
import styles from "./Markdown.module.scss"; import styles from "./Markdown.module.scss";
@ -64,8 +60,6 @@ export const md: MarkdownIt = MarkdownIt({
.disable("image") .disable("image")
.use(MarkdownEmoji, { defs: emojiDictionary }) .use(MarkdownEmoji, { defs: emojiDictionary })
.use(MarkdownSpoilers) .use(MarkdownSpoilers)
.use(MarkdownSup)
.use(MarkdownSub)
.use(MarkdownKatex, { .use(MarkdownKatex, {
throwOnError: false, throwOnError: false,
maxExpand: 0, maxExpand: 0,
@ -129,7 +123,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const { openLink } = useIntermediate(); const { openLink } = useIntermediate();
if (typeof content === "undefined") return null; if (typeof content === "undefined") return null;
if (content.length === 0) return null; if (!content || content.length === 0) return null;
// We replace the message with the mention at the time of render. // We replace the message with the mention at the time of render.
// We don't care if the mention changes. // We don't care if the mention changes.

View file

@ -1,9 +1,7 @@
import { X } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import { Crown } from "@styled-icons/boxicons-solid"; import { Crown } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Presence } from "revolt-api/types/Users"; import { User, Channel } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Item.module.scss"; import styles from "./Item.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@ -65,7 +63,7 @@ export const UserButton = observer((props: UserProps) => {
data-alert={typeof alert === "string"} data-alert={typeof alert === "string"}
data-online={ data-online={
typeof channel !== "undefined" || typeof channel !== "undefined" ||
(user.online && user.status?.presence !== Presence.Invisible) (user.online && user.status?.presence !== "Invisible")
} }
{...useTriggerEvents("Menu", { {...useTriggerEvents("Menu", {
user: user._id, user: user._id,

View file

@ -4,12 +4,14 @@ import { useContext } from "preact/hooks";
import { import {
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient"; } from "../../../context/revoltjs/RevoltClient";
import Banner from "../../ui/Banner"; import Banner from "../../ui/Banner";
export default function ConnectionStatus() { export default function ConnectionStatus() {
const status = useContext(StatusContext); const status = useContext(StatusContext);
const client = useClient();
if (status === ClientStatus.OFFLINE) { if (status === ClientStatus.OFFLINE) {
return ( return (
@ -20,7 +22,10 @@ export default function ConnectionStatus() {
} else if (status === ClientStatus.DISCONNECTED) { } else if (status === ClientStatus.DISCONNECTED) {
return ( return (
<Banner> <Banner>
<Text id="app.special.status.disconnected" /> <Text id="app.special.status.disconnected" /> <br />
<a onClick={() => client.websocket.connect()}>
<Text id="app.special.status.reconnect" />
</a>
</Banner> </Banner>
); );
} else if (status === ClientStatus.CONNECTING) { } else if (status === ClientStatus.CONNECTING) {

View file

@ -6,7 +6,6 @@ import {
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useLocation, useParams } from "react-router-dom"; import { Link, useLocation, useParams } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -47,14 +46,16 @@ export default observer(() => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const client = useContext(AppContext); const client = useContext(AppContext);
const state = useApplicationState(); const state = useApplicationState();
const { channel: currentChannel } = useParams<{ channel: string }>(); const { channel: channel_id } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const channels = [...client.channels.values()].filter( const channels = [...client.channels.values()].filter(
(x) => x.channel_type === "DirectMessage" || x.channel_type === "Group", (x) =>
(x.channel_type === "DirectMessage" && x.active) ||
x.channel_type === "Group",
); );
const obj = client.channels.get(currentChannel); const channel = client.channels.get(channel_id);
// ! FIXME: move this globally // ! FIXME: move this globally
// Track what page the user was last on (in home page). // Track what page the user was last on (in home page).
@ -66,7 +67,7 @@ export default observer(() => {
// ! FIXME: must be a better way // ! FIXME: must be a better way
const incoming = [...client.users.values()].filter( const incoming = [...client.users.values()].filter(
(user) => user?.relationship === RelationshipStatus.Incoming, (user) => user?.relationship === "Incoming",
); );
return ( return (
@ -104,9 +105,10 @@ export default observer(() => {
</> </>
)} )}
<ConditionalLink <ConditionalLink
active={obj?.channel_type === "SavedMessages"} active={channel?.channel_type === "SavedMessages"}
to="/open/saved"> to="/open/saved">
<ButtonItem active={obj?.channel_type === "SavedMessages"}> <ButtonItem
active={channel?.channel_type === "SavedMessages"}>
<Notepad size={20} /> <Notepad size={20} />
<span> <span>
<Text id="app.navigation.tabs.saved" /> <Text id="app.navigation.tabs.saved" />
@ -152,7 +154,7 @@ export default observer(() => {
return ( return (
<ConditionalLink <ConditionalLink
key={channel._id} key={channel._id}
active={channel._id === currentChannel} active={channel._id === channel_id}
to={`/channel/${channel._id}`}> to={`/channel/${channel._id}`}>
<ChannelButton <ChannelButton
user={user} user={user}
@ -165,7 +167,7 @@ export default observer(() => {
: undefined : undefined
} }
alertCount={mentionCount} alertCount={mentionCount}
active={channel._id === currentChannel} active={channel._id === channel_id}
/> />
</ConditionalLink> </ConditionalLink>
); );

View file

@ -2,10 +2,8 @@ import { Plus } from "@styled-icons/boxicons-regular";
import { Cog, Compass } from "@styled-icons/boxicons-solid"; import { Cog, Compass } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useHistory, useLocation, useParams } from "react-router-dom"; import { Link, useHistory, useLocation, useParams } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Ref } from "preact";
import { useTriggerEvents } from "preact-context-menu"; import { useTriggerEvents } from "preact-context-menu";
import ConditionalLink from "../../../lib/ConditionalLink"; import ConditionalLink from "../../../lib/ConditionalLink";
@ -248,7 +246,7 @@ export default observer(() => {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
let alertCount = [...client.users.values()].filter( let alertCount = [...client.users.values()].filter(
(x) => x.relationship === RelationshipStatus.Incoming, (x) => x.relationship === "Incoming",
).length; ).length;
const homeActive = const homeActive =
@ -290,7 +288,7 @@ export default observer(() => {
{channels {channels
.filter( .filter(
(x) => (x) =>
(x.channel_type === "DirectMessage" || ((x.channel_type === "DirectMessage" && x.active) ||
x.channel_type === "Group") && x.channel_type === "Group") &&
x.unread, x.unread,
) )

View file

@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router"; import { Redirect, useParams } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Ref } from "preact"; import { Ref } from "preact";

View file

@ -1,6 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import { getRenderer } from "../../../lib/renderer/Singleton"; import { getRenderer } from "../../../lib/renderer/Singleton";

View file

@ -1,7 +1,6 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { GroupedVirtuoso } from "react-virtuoso"; import { GroupedVirtuoso } from "react-virtuoso";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel, User } from "revolt.js";
import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View file

@ -2,11 +2,7 @@
import { autorun } from "mobx"; import { autorun } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Role } from "revolt-api/types/Servers"; import { Channel, Server, User, API } from "revolt.js";
import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
@ -62,7 +58,7 @@ function useEntries(
.map((id) => { .map((id) => {
return [id, roles![id], roles![id].rank ?? 0] as [ return [id, roles![id], roles![id].rank ?? 0] as [
string, string,
Role, API.Role,
number, number,
]; ];
}) })
@ -96,7 +92,7 @@ function useEntries(
const sort = member?.nickname ?? u.username; const sort = member?.nickname ?? u.username;
const entry = [u, sort] as [User, string]; const entry = [u, sort] as [User, string];
if (!u.online || u.status?.presence === Presence.Invisible) { if (!u.online || u.status?.presence === "Invisible") {
categories.offline.push(entry); categories.offline.push(entry);
} else { } else {
if (isServer) { if (isServer) {
@ -164,7 +160,7 @@ function useEntries(
useEffect(() => { useEffect(() => {
return autorun(() => sort(generateKeys())); return autorun(() => sort(generateKeys()));
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, [channel]);
return entries; return entries;
} }

View file

@ -1,5 +1,5 @@
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { Message as MessageI } from "revolt.js/dist/maps/Messages"; import { Message as MessageI } from "revolt.js";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View file

@ -0,0 +1,45 @@
import { API, Channel, Member, Server } from "revolt.js";
import { Permission } from "revolt.js";
import { PermissionSelect } from "./PermissionSelect";
interface Props {
value: API.OverrideField | number;
onChange: (v: API.OverrideField | number) => void;
target?: Channel | Server;
filter?: (keyof typeof Permission)[];
}
export function PermissionList({ value, onChange, filter, target }: Props) {
return (
<>
{(Object.keys(Permission) as (keyof typeof Permission)[])
.filter(
(key) =>
![
"GrantAllSafe",
"TimeoutMembers",
"ReadMessageHistory",
"Speak",
"Video",
"MuteMembers",
"DeafenMembers",
"MoveMembers",
"ManageWebhooks",
].includes(key) &&
(!filter || filter.includes(key)),
)
.map((x) => (
<PermissionSelect
id={x}
key={x}
permission={Permission[x]}
value={value}
onChange={onChange}
target={target}
/>
))}
</>
);
}

View file

@ -0,0 +1,151 @@
import { Lock } from "@styled-icons/boxicons-solid";
import Long from "long";
import { API, Channel, Member, Server } from "revolt.js";
import { Permission } from "revolt.js";
import styled, { css } from "styled-components";
import { Text } from "preact-i18n";
import { useMemo } from "preact/hooks";
import Checkbox from "../../ui/Checkbox";
import { OverrideSwitch } from "@revoltchat/ui";
interface PermissionSelectProps {
id: keyof typeof Permission;
target?: Channel | Server;
permission: number;
value: API.OverrideField | number;
onChange: (value: API.OverrideField | number) => void;
}
type State = "Allow" | "Neutral" | "Deny";
const PermissionEntry = styled.label<{ disabled?: boolean }>`
gap: 8px;
width: 100%;
margin: 8px 0;
display: flex;
font-size: 1.1em;
align-items: center;
.title {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.lock {
margin-inline-start: 4px;
}
.description {
font-size: 0.8em;
color: var(--secondary-foreground);
}
${(props) =>
props.disabled &&
css`
color: var(--tertiary-foreground);
`}
`;
export function PermissionSelect({
id,
permission,
value,
onChange,
target,
}: PermissionSelectProps) {
const state: State = useMemo(() => {
if (typeof value === "object") {
if (Long.fromNumber(value.d).and(permission).eq(permission)) {
return "Deny";
}
if (Long.fromNumber(value.a).and(permission).eq(permission)) {
return "Allow";
}
return "Neutral";
} else {
if (Long.fromNumber(value).and(permission).eq(permission)) {
return "Allow";
}
return "Neutral";
}
}, [value]);
function onSwitch(state: State) {
if (typeof value !== "object") throw "!";
// Convert to Long so we can do bitwise ops.
let allow = Long.fromNumber(value.a);
let deny = Long.fromNumber(value.d);
// Clear the current permission value.
if (allow.and(permission).eq(permission)) {
allow = allow.xor(permission);
}
if (deny.and(permission).eq(permission)) {
deny = deny.xor(permission);
}
// Apply the current permission state.
if (state === "Allow") {
allow = allow.or(permission);
}
if (state === "Deny") {
deny = deny.or(permission);
}
// Invoke state change.
onChange({
a: allow.toNumber(),
d: deny.toNumber(),
});
}
const member =
target &&
(target instanceof Server ? target.member : target.server?.member);
const disabled = member && !member.hasPermission(target!, id);
return (
<PermissionEntry disabled={disabled}>
<span class="title">
<span>
<Text id={`permissions.${id}.t`}>{id}</Text>
{disabled && <Lock className="lock" size={14} />}
</span>
<span class="description">
<Text id={`permissions.${id}.d`} />
</span>
</span>
{typeof value === "object" ? (
<OverrideSwitch
disabled={disabled}
state={state}
onChange={onSwitch}
/>
) : (
<Checkbox
disabled={disabled}
checked={state === "Allow"}
onChange={() =>
onChange(
Long.fromNumber(value, false)
.xor(permission)
.toNumber(),
)
}
/>
)}
</PermissionEntry>
);
}

View file

@ -0,0 +1,12 @@
import { API } from "revolt.js";
export type RoleOrDefault = (
| API.Role
| {
name: string;
permissions: number;
colour?: string;
hoist?: boolean;
rank?: number;
}
) & { id: string };

View file

@ -89,7 +89,7 @@ export interface CheckboxProps {
disabled?: boolean; disabled?: boolean;
contrast?: boolean; contrast?: boolean;
className?: string; className?: string;
children: Children; children?: Children;
description?: Children; description?: Children;
onChange: (state: boolean) => void; onChange: (state: boolean) => void;
} }

View file

@ -1,3 +1,4 @@
// @ts-expect-error No typings.
import rgba from "color-rgba"; import rgba from "color-rgba";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
@ -102,7 +103,7 @@ export const FONTS: Record<Fonts, { name: string; load: () => void }> = {
}, },
}, },
"OpenDyslexic": { OpenDyslexic: {
name: "OpenDyslexic", name: "OpenDyslexic",
load: async () => { load: async () => {
await import("@fontsource/opendyslexic/400.css"); await import("@fontsource/opendyslexic/400.css");
@ -319,6 +320,14 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
:root { :root {
${(props) => generateVariables(props.theme)} ${(props) => generateVariables(props.theme)}
} }
${(props) =>
props.theme["min-opacity"] === 1 &&
`
* {
backdrop-filter: unset !important;
}
`}
`; `;
export const generateVariables = (theme: Theme) => { export const generateVariables = (theme: Theme) => {

View file

@ -1,13 +1,6 @@
import { Prompt } from "react-router"; import { Prompt } from "react-router";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import type { Attachment } from "revolt-api/types/Autumn"; import { API, Channel, Message, Server, User } from "revolt.js";
import { Bot } from "revolt-api/types/Bots";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels";
import type { EmbedImage } from "revolt-api/types/January";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { createContext } from "preact"; import { createContext } from "preact";
import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { useContext, useEffect, useMemo, useState } from "preact/hooks";
@ -31,6 +24,7 @@ export type Screen =
| { id: "clipboard"; text: string } | { id: "clipboard"; text: string }
| { id: "token_reveal"; token: string; username: string } | { id: "token_reveal"; token: string; username: string }
| { id: "external_link_prompt"; link: string } | { id: "external_link_prompt"; link: string }
| { id: "sessions", confirm: () => void }
| { | {
id: "_prompt"; id: "_prompt";
question: Children; question: Children;
@ -61,7 +55,11 @@ export type Screen =
| { | {
type: "create_channel"; type: "create_channel";
target: Server; target: Server;
cb?: (channel: TextChannel | VoiceChannel) => void; cb?: (
channel: Channel & {
channel_type: "TextChannel" | "VoiceChannel";
},
) => void;
} }
| { type: "create_category"; target: Server } | { type: "create_category"; target: Server }
)) ))
@ -101,11 +99,11 @@ export type Screen =
omit?: string[]; omit?: string[];
callback: (users: string[]) => Promise<void>; callback: (users: string[]) => Promise<void>;
} }
| { id: "image_viewer"; attachment?: Attachment; embed?: EmbedImage } | { id: "image_viewer"; attachment?: API.File; embed?: API.Image }
| { id: "channel_info"; channel: Channel } | { id: "channel_info"; channel: Channel }
| { id: "pending_requests"; users: User[] } | { id: "pending_requests"; users: User[] }
| { id: "modify_account"; field: "username" | "email" | "password" } | { id: "modify_account"; field: "username" | "email" | "password" }
| { id: "create_bot"; onCreate: (bot: Bot) => void } | { id: "create_bot"; onCreate: (bot: API.Bot) => void }
| { | {
id: "server_identity"; id: "server_identity";
server: Server; server: Server;

View file

@ -9,7 +9,8 @@ import { InputModal } from "./modals/Input";
import { OnboardingModal } from "./modals/Onboarding"; import { OnboardingModal } from "./modals/Onboarding";
import { PromptModal } from "./modals/Prompt"; import { PromptModal } from "./modals/Prompt";
import { SignedOutModal } from "./modals/SignedOut"; import { SignedOutModal } from "./modals/SignedOut";
import {ExternalLinkModal} from "./modals/ExternalLinkPrompt"; import { ExternalLinkModal} from "./modals/ExternalLinkPrompt";
import { SessionsModal } from "./modals/SessionsPrompt";
import { TokenRevealModal } from "./modals/TokenReveal"; import { TokenRevealModal } from "./modals/TokenReveal";
export interface Props { export interface Props {
@ -40,6 +41,8 @@ export default function Modals({ screen, openScreen }: Props) {
return <OnboardingModal onClose={onClose} {...screen} />; return <OnboardingModal onClose={onClose} {...screen} />;
case "external_link_prompt": case "external_link_prompt":
return <ExternalLinkModal onClose={onClose} {...screen} />; return <ExternalLinkModal onClose={onClose} {...screen} />;
case "sessions":
return <SessionsModal onClose={onClose} {...screen} />;
} }
return null; return null;

View file

@ -11,7 +11,7 @@ export function ErrorModal({ onClose, error }: Props) {
return ( return (
<Modal <Modal
visible={true} visible={true}
onClose={() => false} onClose={onClose}
title={<Text id="app.special.modals.error" />} title={<Text id="app.special.modals.error" />}
actions={[ actions={[
{ {

View file

@ -1,5 +1,5 @@
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -69,6 +69,7 @@ export function InputModal({
)} )}
<InputBox <InputBox
value={value} value={value}
style={{ width: "100%" }}
onChange={(e) => setValue(e.currentTarget.value)} onChange={(e) => setValue(e.currentTarget.value)}
/> />
</Modal> </Modal>
@ -101,7 +102,6 @@ export function SpecialInputModal(props: SpecialProps) {
callback={async (name) => { callback={async (name) => {
const group = await client.channels.createGroup({ const group = await client.channels.createGroup({
name, name,
nonce: ulid(),
users: [], users: [],
}); });
@ -130,7 +130,6 @@ export function SpecialInputModal(props: SpecialProps) {
callback={async (name) => { callback={async (name) => {
const server = await client.servers.createServer({ const server = await client.servers.createServer({
name, name,
nonce: ulid(),
}); });
history.push(`/server/${server._id}`); history.push(`/server/${server._id}`);
@ -159,7 +158,7 @@ export function SpecialInputModal(props: SpecialProps) {
onClose={onClose} onClose={onClose}
question={<Text id="app.context_menu.set_custom_status" />} question={<Text id="app.context_menu.set_custom_status" />}
field={<Text id="app.context_menu.custom_status" />} field={<Text id="app.context_menu.custom_status" />}
defaultValue={client.user?.status?.text} defaultValue={client.user?.status?.text ?? undefined}
callback={(text) => callback={(text) =>
client.users.edit({ client.users.edit({
status: { status: {
@ -177,11 +176,8 @@ export function SpecialInputModal(props: SpecialProps) {
onClose={onClose} onClose={onClose}
question={"Add Friend"} question={"Add Friend"}
callback={(username) => callback={(username) =>
client client.api
.req( .put(`/users/${username as ""}/friend`)
"PUT",
`/users/${username}/friend` as "/users/id/friend",
)
.then(undefined) .then(undefined)
} }
/> />

View file

@ -16,6 +16,10 @@
h1 { h1 {
margin: 0; margin: 0;
} }
img {
max-height: 80px;
}
} }
&.form { &.form {

View file

@ -1,10 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { TextChannel, VoiceChannel } from "revolt-api/types/Channels"; import { Channel, Message as MessageI, Server, User } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { ulid } from "ulid"; import { ulid } from "ulid";
import styles from "./Prompt.module.scss"; import styles from "./Prompt.module.scss";
@ -74,7 +70,11 @@ type SpecialProps = { onClose: () => void } & (
| { | {
type: "create_channel"; type: "create_channel";
target: Server; target: Server;
cb?: (channel: TextChannel | VoiceChannel) => void; cb?: (
channel: Channel & {
channel_type: "TextChannel" | "VoiceChannel";
},
) => void;
} }
| { type: "create_category"; target: Server } | { type: "create_category"; target: Server }
); );
@ -254,7 +254,7 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
props.target props.target
.createInvite() .createInvite()
.then((code) => setCode(code)) .then(({ _id }) => setCode(_id))
.catch((err) => setError(takeError(err))) .catch((err) => setError(takeError(err)))
.finally(() => setProcessing(false)); .finally(() => setProcessing(false));
}, [props.target]); }, [props.target]);
@ -429,11 +429,10 @@ export const SpecialPromptModal = observer((props: SpecialProps) => {
await props.target.createChannel({ await props.target.createChannel({
type, type,
name, name,
nonce: ulid(),
}); });
if (props.cb) { if (props.cb) {
props.cb(channel); props.cb(channel as any);
} else { } else {
history.push( history.push(
`/server/${props.target._id}/channel/${channel._id}`, `/server/${props.target._id}/channel/${channel._id}`,

View file

@ -0,0 +1,38 @@
import { Text } from "preact-i18n";
import Modal from "../../../components/ui/Modal";
interface Props {
onClose: () => void;
confirm: () => void;
}
export function SessionsModal({ onClose, confirm}: Props) {
return (
<Modal
visible={true}
onClose={onClose}
title={<Text id={"app.special.modals.sessions.title"} />}
actions={[
{
onClick: () => {
onClose()
},
confirmation: true,
contrast: true,
accent: true,
children: <Text id="app.special.modals.actions.back"/>
},
{
onClick: () => {
confirm()
onClose()
},
confirmation: true,
children: <Text id="app.special.modals.sessions.accept"/>
}
]}>
<Text id="app.special.modals.sessions.short" /> <br />
</Modal>
)
}

View file

@ -1,6 +1,6 @@
import { X } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import styles from "./ChannelInfo.module.scss"; import styles from "./ChannelInfo.module.scss";

View file

@ -1,5 +1,5 @@
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import { Bot } from "revolt-api/types/Bots"; import { API } from "revolt.js";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useState } from "preact/hooks"; import { useContext, useState } from "preact/hooks";
@ -13,7 +13,7 @@ import { takeError } from "../../revoltjs/util";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
onCreate: (bot: Bot) => void; onCreate: (bot: API.Bot) => void;
} }
interface FormInputs { interface FormInputs {

View file

@ -1,6 +1,5 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn"; import { API } from "revolt.js";
import { EmbedImage } from "revolt-api/types/January";
import styles from "./ImageViewer.module.scss"; import styles from "./ImageViewer.module.scss";
@ -12,11 +11,11 @@ import { useClient } from "../../revoltjs/RevoltClient";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
embed?: EmbedImage; embed?: API.Image;
attachment?: Attachment; attachment?: API.File;
} }
type ImageMetadata = AttachmentMetadata & { type: "Image" }; type ImageMetadata = API.Metadata & { type: "Image" };
export function ImageViewer({ attachment, embed, onClose }: Props) { export function ImageViewer({ attachment, embed, onClose }: Props) {
if (attachment && attachment.metadata.type !== "Image") { if (attachment && attachment.metadata.type !== "Image") {

View file

@ -43,19 +43,19 @@ export function ModifyAccountModal({ onClose, field }: Props) {
try { try {
if (field === "email") { if (field === "email") {
await client.req("PATCH", "/auth/account/change/email", { await client.api.patch("/auth/account/change/email", {
current_password: password, current_password: password,
email: new_email, email: new_email,
}); });
onClose(); onClose();
} else if (field === "password") { } else if (field === "password") {
await client.req("PATCH", "/auth/account/change/password", { await client.api.patch("/auth/account/change/password", {
current_password: password, current_password: password,
password: new_password, password: new_password,
}); });
onClose(); onClose();
} else if (field === "username") { } else if (field === "username") {
await client.req("PATCH", "/users/id/username", { await client.api.patch("/users/@me/username", {
username: new_username, username: new_username,
password, password,
}); });

View file

@ -1,5 +1,5 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js";
import styles from "./UserPicker.module.scss"; import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View file

@ -1,11 +1,12 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js";
import styled, { css } from "styled-components/macro";
import styles from "./ServerIdentityModal.module.scss"; import styles from "./ServerIdentityModal.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { noop } from "../../../lib/js";
import Button from "../../../components/ui/Button"; import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox"; import InputBox from "../../../components/ui/InputBox";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
@ -57,8 +58,12 @@ export const ServerIdentityModal = observer(({ server, onClose }: Props) => {
fileType="avatars" fileType="avatars"
behaviour="upload" behaviour="upload"
maxFileSize={4_000_000} maxFileSize={4_000_000}
onUpload={(avatar) => member.edit({ avatar })} onUpload={(avatar) =>
remove={() => member.edit({ remove: "Avatar" })} member.edit({ avatar }).then(noop)
}
remove={() =>
member.edit({ remove: ["Avatar"] }).then(noop)
}
defaultPreview={client.user?.generateAvatarURL( defaultPreview={client.user?.generateAvatarURL(
{ {
max_side: 256, max_side: 256,
@ -92,7 +97,7 @@ export const ServerIdentityModal = observer(({ server, onClose }: Props) => {
<Button <Button
plain plain
onClick={() => onClick={() =>
member.edit({ remove: "Nickname" }) member.edit({ remove: ["Nickname"] })
}> }>
<Text id="app.special.modals.actions.remove" /> <Text id="app.special.modals.actions.remove" />
</Button> </Button>

View file

@ -1,5 +1,3 @@
import { RelationshipStatus } from "revolt-api/types/Users";
import styles from "./UserPicker.module.scss"; import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
@ -37,7 +35,7 @@ export function UserPicker(props: Props) {
.filter( .filter(
(x) => (x) =>
x && x &&
x.relationship === RelationshipStatus.Friend && x.relationship === "Friend" &&
!omit.includes(x._id), !omit.includes(x._id),
) )
.map((x) => ( .map((x) => (

View file

@ -100,6 +100,10 @@
background: var(--primary-background); background: var(--primary-background);
border-radius: 0 0 var(--border-radius) var(--border-radius); border-radius: 0 0 var(--border-radius) var(--border-radius);
.markdown {
user-select: text;
}
.empty { .empty {
display: flex; display: flex;
gap: 10px; gap: 10px;

View file

@ -9,9 +9,7 @@ import {
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { Profile, RelationshipStatus } from "revolt-api/types/Users"; import { UserPermission, API } from "revolt.js";
import { UserPermission } from "revolt.js/dist/api/permissions";
import { Route } from "revolt.js/dist/api/routes";
import styles from "./UserProfile.module.scss"; import styles from "./UserProfile.module.scss";
import { Localizer, Text } from "preact-i18n"; import { Localizer, Text } from "preact-i18n";
@ -44,18 +42,18 @@ interface Props {
user_id: string; user_id: string;
dummy?: boolean; dummy?: boolean;
onClose?: () => void; onClose?: () => void;
dummyProfile?: Profile; dummyProfile?: API.UserProfile;
} }
export const UserProfile = observer( export const UserProfile = observer(
({ user_id, onClose, dummy, dummyProfile }: Props) => { ({ user_id, onClose, dummy, dummyProfile }: Props) => {
const { openScreen, writeClipboard } = useIntermediate(); const { openScreen, writeClipboard } = useIntermediate();
const [profile, setProfile] = useState<undefined | null | Profile>( const [profile, setProfile] = useState<
undefined, undefined | null | API.UserProfile
); >(undefined);
const [mutual, setMutual] = useState< const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"] undefined | null | API.MutualResponse
>(undefined); >(undefined);
const [isPublicBot, setIsPublicBot] = useState< const [isPublicBot, setIsPublicBot] = useState<
undefined | null | boolean undefined | null | boolean
@ -139,7 +137,11 @@ export const UserProfile = observer(
const backgroundURL = const backgroundURL =
profile && profile &&
client.generateFileURL(profile.background, { width: 1000 }, true); client.generateFileURL(
profile.background as any,
{ width: 1000 },
true,
);
const badges = user.badges ?? 0; const badges = user.badges ?? 0;
const flags = user.flags ?? 0; const flags = user.flags ?? 0;
@ -198,7 +200,7 @@ export const UserProfile = observer(
</Button> </Button>
</Link> </Link>
)} )}
{user.relationship === RelationshipStatus.Friend && ( {user.relationship === "Friend" && (
<Localizer> <Localizer>
<Tooltip <Tooltip
content={ content={
@ -214,28 +216,26 @@ export const UserProfile = observer(
</Tooltip> </Tooltip>
</Localizer> </Localizer>
)} )}
{user.relationship === RelationshipStatus.User && {user.relationship === "User" && !dummy && (
!dummy && ( <IconButton
<IconButton onClick={() => {
onClick={() => { onClose?.();
onClose?.(); history.push(`/settings/profile`);
history.push(`/settings/profile`); }}>
}}> <Edit size={28} />
<Edit size={28} /> </IconButton>
</IconButton> )}
)}
{!user.bot && {!user.bot &&
flags != 2 && flags != 2 &&
flags != 4 && flags != 4 &&
(user.relationship === (user.relationship === "Incoming" ||
RelationshipStatus.Incoming || user.relationship === "None" ||
user.relationship === user.relationship === null) && (
RelationshipStatus.None) && (
<IconButton onClick={() => user.addFriend()}> <IconButton onClick={() => user.addFriend()}>
<UserPlus size={28} /> <UserPlus size={28} />
</IconButton> </IconButton>
)} )}
{user.relationship === RelationshipStatus.Outgoing && ( {user.relationship === "Outgoing" && (
<IconButton onClick={() => user.removeFriend()}> <IconButton onClick={() => user.removeFriend()}>
<UserX size={28} /> <UserX size={28} />
</IconButton> </IconButton>
@ -247,7 +247,7 @@ export const UserProfile = observer(
onClick={() => setTab("profile")}> onClick={() => setTab("profile")}>
<Text id="app.special.popovers.user_profile.profile" /> <Text id="app.special.popovers.user_profile.profile" />
</div> </div>
{user.relationship !== RelationshipStatus.User && ( {user.relationship !== "User" && (
<> <>
<div <div
data-active={tab === "friends"} data-active={tab === "friends"}
@ -338,7 +338,9 @@ export const UserProfile = observer(
<Text id="app.special.popovers.user_profile.sub.information" /> <Text id="app.special.popovers.user_profile.sub.information" />
</div> </div>
)} )}
<Markdown content={profile?.content} /> <div className={styles.markdown}>
<Markdown content={profile?.content} />
</div>
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/} {/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
</div> </div>
) : ( ) : (

View file

@ -94,6 +94,7 @@ export function grabFiles(
input.addEventListener("change", async (e) => { input.addEventListener("change", async (e) => {
const files = (e.currentTarget as HTMLInputElement)?.files; const files = (e.currentTarget as HTMLInputElement)?.files;
if (!files) return; if (!files) return;
for (const file of files) { for (const file of files) {
if (file.size > maxFileSize) { if (file.size > maxFileSize) {
return tooLarge(); return tooLarge();
@ -184,6 +185,7 @@ export function FileUploader(props: Props) {
id: "error", id: "error",
error: "FileTooLarge", error: "FileTooLarge",
}); });
continue;
} }
files.push(blob); files.push(blob);
@ -212,6 +214,7 @@ export function FileUploader(props: Props) {
for (const item of dropped) { for (const item of dropped) {
if (item.size > props.maxFileSize) { if (item.size > props.maxFileSize) {
openScreen({ id: "error", error: "FileTooLarge" }); openScreen({ id: "error", error: "FileTooLarge" });
continue;
} }
files.push(item); files.push(item);

View file

@ -1,7 +1,5 @@
import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Presence, RelationshipStatus } from "revolt-api/types/Users"; import { Message, User } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js/dist/maps/Users";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { useCallback, useContext, useEffect } from "preact/hooks"; import { useCallback, useContext, useEffect } from "preact/hooks";
@ -84,7 +82,7 @@ function Notifier() {
} }
let body, icon; let body, icon;
if (typeof msg.content === "string") { if (msg.content) {
body = client.markdownToText(msg.content); body = client.markdownToText(msg.content);
if (msg.masquerade?.avatar) { if (msg.masquerade?.avatar) {
@ -92,22 +90,23 @@ function Notifier() {
} else { } else {
icon = msg.author?.generateAvatarURL({ max_side: 256 }); icon = msg.author?.generateAvatarURL({ max_side: 256 });
} }
} else { } else if (msg.system) {
const users = client.users; const users = client.users;
switch (msg.content.type) {
switch (msg.system.type) {
case "user_added": case "user_added":
case "user_remove": case "user_remove":
{ {
const user = users.get(msg.content.id); const user = users.get(msg.system.id);
body = translate( body = translate(
`app.main.channel.system.${ `app.main.channel.system.${
msg.content.type === "user_added" msg.system.type === "user_added"
? "added_by" ? "added_by"
: "removed_by" : "removed_by"
}`, }`,
{ {
user: user?.username, user: user?.username,
other_user: users.get(msg.content.by) other_user: users.get(msg.system.by)
?.username, ?.username,
}, },
); );
@ -121,9 +120,9 @@ function Notifier() {
case "user_kicked": case "user_kicked":
case "user_banned": case "user_banned":
{ {
const user = users.get(msg.content.id); const user = users.get(msg.system.id);
body = translate( body = translate(
`app.main.channel.system.${msg.content.type}`, `app.main.channel.system.${msg.system.type}`,
{ user: user?.username }, { user: user?.username },
); );
icon = user?.generateAvatarURL({ icon = user?.generateAvatarURL({
@ -133,12 +132,12 @@ function Notifier() {
break; break;
case "channel_renamed": case "channel_renamed":
{ {
const user = users.get(msg.content.by); const user = users.get(msg.system.by);
body = translate( body = translate(
`app.main.channel.system.channel_renamed`, `app.main.channel.system.channel_renamed`,
{ {
user: users.get(msg.content.by)?.username, user: users.get(msg.system.by)?.username,
name: msg.content.name, name: msg.system.name,
}, },
); );
icon = user?.generateAvatarURL({ icon = user?.generateAvatarURL({
@ -149,10 +148,10 @@ function Notifier() {
case "channel_description_changed": case "channel_description_changed":
case "channel_icon_changed": case "channel_icon_changed":
{ {
const user = users.get(msg.content.by); const user = users.get(msg.system.by);
body = translate( body = translate(
`app.main.channel.system.${msg.content.type}`, `app.main.channel.system.${msg.system.type}`,
{ user: users.get(msg.content.by)?.username }, { user: users.get(msg.system.by)?.username },
); );
icon = user?.generateAvatarURL({ icon = user?.generateAvatarURL({
max_side: 256, max_side: 256,
@ -210,17 +209,17 @@ function Notifier() {
const relationship = useCallback( const relationship = useCallback(
async (user: User) => { async (user: User) => {
if (client.user?.status?.presence === Presence.Busy) return; if (client.user?.status?.presence === "Busy") return;
if (!showNotification) return; if (!showNotification) return;
let event; let event;
switch (user.relationship) { switch (user.relationship) {
case RelationshipStatus.Incoming: case "Incoming":
event = translate("notifications.sent_request", { event = translate("notifications.sent_request", {
person: user.username, person: user.username,
}); });
break; break;
case RelationshipStatus.Friend: case "Friend":
event = translate("notifications.now_friends", { event = translate("notifications.now_friends", {
person: user.username, person: user.username,
}); });

View file

@ -50,7 +50,7 @@ export default observer(({ children }: Props) => {
useEffect(() => { useEffect(() => {
if (navigator.onLine) { if (navigator.onLine) {
state.config.createClient().req("GET", "/").then(state.config.set); state.config.createClient().api.get("/").then(state.config.set);
} }
}, []); }, []);
@ -79,7 +79,7 @@ export default observer(({ children }: Props) => {
} }
}, [state.auth.getSession()]); }, [state.auth.getSession()]);
useEffect(() => registerEvents(state.auth, setStatus, client), [client]); useEffect(() => registerEvents(state, setStatus, client), [client]);
if (!loaded || status === ClientStatus.LOADING) { if (!loaded || status === ClientStatus.LOADING) {
return <Preloader type="spinner" />; return <Preloader type="spinner" />;

View file

@ -1,7 +1,7 @@
/** /**
* This file monitors the message cache to delete any queued messages that have already sent. * This file monitors the message cache to delete any queued messages that have already sent.
*/ */
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";

View file

@ -1,7 +1,7 @@
/** /**
* This file monitors changes to settings and syncs them to the server. * This file monitors changes to settings and syncs them to the server.
*/ */
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; import { ClientboundNotification } from "revolt.js";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";

View file

@ -1,14 +1,16 @@
import { Client } from "revolt.js/dist"; import { Client, Server } from "revolt.js";
import { StateUpdater } from "preact/hooks"; import { StateUpdater } from "preact/hooks";
import Auth from "../../mobx/stores/Auth"; import { deleteRenderer } from "../../lib/renderer/Singleton";
import State from "../../mobx/State";
import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar"; import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar";
import { ClientStatus } from "./RevoltClient"; import { ClientStatus } from "./RevoltClient";
export function registerEvents( export function registerEvents(
auth: Auth, state: State,
setStatus: StateUpdater<ClientStatus>, setStatus: StateUpdater<ClientStatus>,
client: Client, client: Client,
) { ) {
@ -25,9 +27,22 @@ export function registerEvents(
}, },
logout: () => { logout: () => {
auth.logout(); state.auth.logout();
state.reset();
setStatus(ClientStatus.READY); 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) { if (import.meta.env.DEV) {

View file

@ -1,4 +1,4 @@
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -6,23 +6,27 @@ import { Children } from "../../types/Preact";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function takeError(error: any): string { export function takeError(error: any): string {
const type = error?.response?.data?.type; if (error.response) {
const id = type; const status = error.response.status;
if (!type) { if (error.response.type) {
if ( return error.response.type;
error?.response?.status === 401 ||
error?.response?.status === 403
) {
return "Unauthorized";
} else if (error && !!error.isAxiosError && !error.response) {
return "NetworkError";
} }
console.error(error); switch (status) {
return "UnknownError"; case 429:
return "TooManyRequests";
case 401:
case 403:
return "Unauthorized";
default:
return "UnknownError";
}
} else if (error.request) {
return "NetworkError";
} }
return id; console.error(error);
return "UnknownError";
} }
export function getChannelName( export function getChannelName(

1
src/globals.d.ts vendored
View file

@ -4,6 +4,7 @@ type NativeConfig = {
frame: boolean; frame: boolean;
build: Build; build: Build;
discordRPC: boolean; discordRPC: boolean;
minimiseToTray: boolean;
hardwareAcceleration: boolean; hardwareAcceleration: boolean;
}; };

View file

@ -11,18 +11,10 @@ import {
Trash, Trash,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { Cog, UserVoice } from "@styled-icons/boxicons-solid"; import { Cog, UserVoice } from "@styled-icons/boxicons-solid";
import { isFirefox } from "react-device-detect";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Attachment } from "revolt-api/types/Autumn"; import { Channel, Message, Server, User, API } from "revolt.js";
import { Presence, RelationshipStatus } from "revolt-api/types/Users"; import { Permission, UserPermission } from "revolt.js";
import {
ChannelPermission,
ServerPermission,
UserPermission,
} from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { import {
ContextMenuWithData, ContextMenuWithData,
@ -60,7 +52,7 @@ interface ContextMenuData {
server_list?: string; server_list?: string;
channel?: string; channel?: string;
message?: Message; message?: Message;
attachment?: Attachment; attachment?: API.File;
unread?: boolean; unread?: boolean;
queued?: QueuedMessage; queued?: QueuedMessage;
@ -82,9 +74,9 @@ type Action =
| { action: "quote_message"; content: string } | { action: "quote_message"; content: string }
| { action: "edit_message"; id: string } | { action: "edit_message"; id: string }
| { action: "delete_message"; target: Message } | { action: "delete_message"; target: Message }
| { action: "open_file"; attachment: Attachment } | { action: "open_file"; attachment: API.File }
| { action: "save_file"; attachment: Attachment } | { action: "save_file"; attachment: API.File }
| { action: "copy_file_link"; attachment: Attachment } | { action: "copy_file_link"; attachment: API.File }
| { action: "open_link"; link: string } | { action: "open_link"; link: string }
| { action: "copy_link"; link: string } | { action: "copy_link"; link: string }
| { action: "remove_member"; channel: Channel; user: User } | { action: "remove_member"; channel: Channel; user: User }
@ -97,7 +89,7 @@ type Action =
| { action: "add_friend"; user: User } | { action: "add_friend"; user: User }
| { action: "remove_friend"; user: User } | { action: "remove_friend"; user: User }
| { action: "cancel_friend"; user: User } | { action: "cancel_friend"; user: User }
| { action: "set_presence"; presence: Presence } | { action: "set_presence"; presence: API.Presence }
| { action: "set_status" } | { action: "set_status" }
| { action: "clear_status" } | { action: "clear_status" }
| { action: "create_channel"; target: Server } | { action: "create_channel"; target: Server }
@ -295,7 +287,7 @@ export default function ContextMenus() {
"attachments", "attachments",
"attachments/download", "attachments/download",
), ),
"_blank", isFirefox || window.native ? "_blank" : "_self",
); );
} }
break; break;
@ -505,9 +497,8 @@ export default function ContextMenus() {
if (server_list) { if (server_list) {
const server = client.servers.get(server_list)!; const server = client.servers.get(server_list)!;
const permissions = server.permission;
if (server) { if (server) {
if (permissions & ServerPermission.ManageChannels) { if (server.havePermission("ManageChannel")) {
generateAction({ generateAction({
action: "create_category", action: "create_category",
target: server, target: server,
@ -517,7 +508,8 @@ export default function ContextMenus() {
target: server, target: server,
}); });
} }
if (permissions & ServerPermission.ManageServer)
if (server.havePermission("ManageServer"))
generateAction({ generateAction({
action: "open_server_settings", action: "open_server_settings",
id: server_list, id: server_list,
@ -589,64 +581,70 @@ export default function ContextMenus() {
} }
if (user) { if (user) {
if (!user.bot) { let actions: (Action["action"] | boolean)[];
let actions: Action["action"][]; switch (user.relationship) {
switch (user.relationship) { case "User":
case RelationshipStatus.User: actions = [];
actions = []; break;
break; case "Friend":
case RelationshipStatus.Friend: actions = [
actions = ["remove_friend", "block_user"]; !user.bot && "remove_friend",
break; "block_user",
case RelationshipStatus.Incoming: ];
break;
case "Incoming":
actions = [
"add_friend",
"cancel_friend",
"block_user",
];
break;
case "Outgoing":
actions = [
!user.bot && "cancel_friend",
"block_user",
];
break;
case "Blocked":
actions = ["unblock_user"];
break;
case "BlockedOther":
actions = ["block_user"];
break;
case "None":
default:
if ((user.flags && 2) || (user.flags && 4)) {
actions = ["block_user"];
} else {
actions = [ actions = [
"add_friend", !user.bot && "add_friend",
"cancel_friend",
"block_user", "block_user",
]; ];
break; }
case RelationshipStatus.Outgoing: }
actions = ["cancel_friend", "block_user"];
break;
case RelationshipStatus.Blocked:
actions = ["unblock_user"];
break;
case RelationshipStatus.BlockedOther:
actions = ["block_user"];
break;
case RelationshipStatus.None:
default:
if (
(user.flags && 2) ||
(user.flags && 4)
) {
actions = ["block_user"];
} else {
actions = ["add_friend", "block_user"];
}
}
if (userPermissions & UserPermission.ViewProfile) { if (userPermissions & UserPermission.ViewProfile) {
generateAction({ generateAction({
action: "view_profile", action: "view_profile",
user, user,
}); });
} }
if ( if (
user._id !== userId && user._id !== userId &&
userPermissions & UserPermission.SendMessage userPermissions & UserPermission.SendMessage
) { ) {
generateAction({ generateAction({
action: "message_user", action: "message_user",
user, user,
}); });
} }
for (let i = 0; i < actions.length; i++) { for (let i = 0; i < actions.length; i++) {
// Typescript can't determine that user the actions are linked together correctly let action = actions[i];
if (action) {
generateAction({ generateAction({
action: actions[i], action,
user, user,
} as unknown as Action); } as unknown as Action);
} }
@ -673,9 +671,7 @@ export default function ContextMenus() {
userId !== uid && userId !== uid &&
uid !== server.owner uid !== server.owner
) { ) {
if ( if (serverPermissions & Permission.KickMembers)
serverPermissions & ServerPermission.KickMembers
)
generateAction( generateAction(
{ {
action: "kick_member", action: "kick_member",
@ -688,7 +684,7 @@ export default function ContextMenus() {
"var(--error)", // the only relevant part really "var(--error)", // the only relevant part really
); );
if (serverPermissions & ServerPermission.BanMembers) if (serverPermissions & Permission.BanMembers)
generateAction( generateAction(
{ {
action: "ban_member", action: "ban_member",
@ -718,8 +714,7 @@ export default function ContextMenus() {
if (message && !queued) { if (message && !queued) {
const sendPermission = const sendPermission =
message.channel && message.channel &&
message.channel.permission & message.channel.permission & Permission.SendMessage;
ChannelPermission.SendMessage;
if (sendPermission) { if (sendPermission) {
generateAction({ generateAction({
@ -759,8 +754,7 @@ export default function ContextMenus() {
if ( if (
message.author_id === userId || message.author_id === userId ||
channelPermissions & channelPermissions & Permission.ManageMessages
ChannelPermission.ManageMessages
) { ) {
generateAction({ generateAction({
action: "delete_message", action: "delete_message",
@ -903,7 +897,7 @@ export default function ContextMenus() {
case "VoiceChannel": case "VoiceChannel":
if ( if (
channelPermissions & channelPermissions &
ChannelPermission.InviteOthers Permission.InviteOthers
) { ) {
generateAction({ generateAction({
action: "create_invite", action: "create_invite",
@ -913,7 +907,7 @@ export default function ContextMenus() {
if ( if (
serverPermissions & serverPermissions &
ServerPermission.ManageServer Permission.ManageServer
) )
generateAction( generateAction(
{ {
@ -926,7 +920,7 @@ export default function ContextMenus() {
if ( if (
serverPermissions & serverPermissions &
ServerPermission.ManageChannels Permission.ManageChannel
) )
generateAction({ generateAction({
action: "delete_channel", action: "delete_channel",
@ -958,20 +952,15 @@ export default function ContextMenus() {
); );
if ( if (
serverPermissions & serverPermissions & Permission.ChangeNickname ||
ServerPermission.ChangeNickname || serverPermissions & Permission.ChangeAvatar
serverPermissions &
ServerPermission.ChangeAvatar
) )
generateAction( generateAction(
{ action: "edit_identity", target: server }, { action: "edit_identity", target: server },
"edit_identity", "edit_identity",
); );
if ( if (serverPermissions & Permission.ManageServer)
serverPermissions &
ServerPermission.ManageServer
)
generateAction( generateAction(
{ {
action: "open_server_settings", action: "open_server_settings",
@ -1060,7 +1049,7 @@ export default function ContextMenus() {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Presence.Online, presence: "Online",
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator online" /> <div className="indicator online" />
@ -1069,7 +1058,7 @@ export default function ContextMenus() {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Presence.Idle, presence: "Idle",
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator idle" /> <div className="indicator idle" />
@ -1078,7 +1067,7 @@ export default function ContextMenus() {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Presence.Busy, presence: "Busy",
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator busy" /> <div className="indicator busy" />
@ -1087,7 +1076,7 @@ export default function ContextMenus() {
<MenuItem <MenuItem
data={{ data={{
action: "set_presence", action: "set_presence",
presence: Presence.Invisible, presence: "Invisible",
}} }}
disabled={!isOnline}> disabled={!isOnline}>
<div className="indicator invisible" /> <div className="indicator invisible" />

View file

@ -9,8 +9,8 @@ import {
LeftArrowAlt, LeftArrowAlt,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import { Server } from "revolt.js/dist/maps/Servers"; import { Server } from "revolt.js";
import { ContextMenuWithData, MenuItem } from "preact-context-menu"; import { ContextMenuWithData, MenuItem } from "preact-context-menu";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View file

@ -1,8 +1,8 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { action, makeAutoObservable } from "mobx"; import { action, makeAutoObservable } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js";
import { Nullable } from "revolt.js/dist/util/null"; import { Nullable } from "revolt.js";
import { SimpleRenderer } from "./simple/SimpleRenderer"; import { SimpleRenderer } from "./simple/SimpleRenderer";
import { RendererRoutines, ScrollState } from "./types"; import { RendererRoutines, ScrollState } from "./types";
@ -222,3 +222,7 @@ export function getRenderer(channel: Channel) {
return renderer; return renderer;
} }
export function deleteRenderer(channel_id: string) {
delete renderers[channel_id];
}

View file

@ -1,4 +1,4 @@
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js";
import { ChannelRenderer } from "./Singleton"; import { ChannelRenderer } from "./Singleton";

View file

@ -4,6 +4,8 @@ import { types } from "mediasoup-client";
import { Device, Producer, Transport } from "mediasoup-client/lib/types"; import { Device, Producer, Transport } from "mediasoup-client/lib/types";
import { useApplicationState } from "../../mobx/State";
import Signaling from "./Signaling"; import Signaling from "./Signaling";
import { import {
ProduceType, ProduceType,
@ -58,6 +60,8 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
this.isDeaf = false; this.isDeaf = false;
const state = useApplicationState();
this.signaling.on( this.signaling.on(
"data", "data",
(json) => { (json) => {
@ -65,11 +69,13 @@ export default class VoiceClient extends EventEmitter<VoiceEvents> {
switch (json.type) { switch (json.type) {
case WSEventType.UserJoined: { case WSEventType.UserJoined: {
this.participants.set(data.id, {}); this.participants.set(data.id, {});
state.settings.sounds.playSound("call_join");
this.emit("userJoined", data.id); this.emit("userJoined", data.id);
break; break;
} }
case WSEventType.UserLeft: { case WSEventType.UserLeft: {
this.participants.delete(data.id); this.participants.delete(data.id);
state.settings.sounds.playSound("call_leave");
this.emit("userLeft", data.id); this.emit("userLeft", data.id);
if (this.recvTransport) this.stopConsume(data.id); if (this.recvTransport) this.stopConsume(data.id);

View file

@ -1,6 +1,6 @@
import { action, makeAutoObservable, runInAction } from "mobx"; import { action, makeAutoObservable, runInAction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Channel } from "revolt.js";
import { Nullable, toNullable } from "revolt.js/dist/util/null"; import { Nullable, toNullable } from "revolt.js";
import type { ProduceType, VoiceUser } from "./Types"; import type { ProduceType, VoiceUser } from "./Types";
import type VoiceClient from "./VoiceClient"; import type VoiceClient from "./VoiceClient";

View file

@ -1,7 +1,7 @@
// @ts-expect-error No typings. // @ts-expect-error No typings.
import stringify from "json-stringify-deterministic"; import stringify from "json-stringify-deterministic";
import localforage from "localforage"; import localforage from "localforage";
import { makeAutoObservable, reaction } from "mobx"; import { makeAutoObservable, reaction, runInAction } from "mobx";
import { Client } from "revolt.js"; import { Client } from "revolt.js";
import { reportError } from "../lib/ErrorBoundary"; import { reportError } from "../lib/ErrorBoundary";
@ -184,10 +184,12 @@ export default class State {
} }
if (Object.keys(obj).length > 0) { if (Object.keys(obj).length > 0) {
client.syncSetSettings( if (client.websocket.connected) {
obj as any, client.syncSetSettings(
revision, obj as any,
); revision,
);
}
} }
break; break;
} }
@ -198,12 +200,14 @@ export default class State {
} }
this.sync.setRevision(id, revision); this.sync.setRevision(id, revision);
client.syncSetSettings( if (client.websocket.connected) {
( client.syncSetSettings(
store as unknown as Syncable (
).toSyncable(), store as unknown as Syncable
revision, ).toSyncable(),
); revision,
);
}
} }
} }
} }
@ -263,6 +267,26 @@ export default class State {
// Post-hydration, init plugins. // Post-hydration, init plugins.
this.plugins.init(); this.plugins.init();
} }
/**
* Reset known state values.
*/
reset() {
runInAction(() => {
this.draft = new Draft();
this.experiments = new Experiments();
this.layout = new Layout();
this.notifications = new NotificationOptions();
this.queue = new MessageQueue();
this.settings = new Settings();
this.sync = new Sync(this);
this.save();
this.persistent = [];
this.register();
});
}
} }
var state: State; var state: State;

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