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
.env
.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
description: File a bug report
title: "[Bug Report]"
title: "bug: "
labels: ["bug"]
body:
- type: markdown
@ -52,7 +52,7 @@ body:
id: desktop
attributes:
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:
- label: Yes, this bug is specific to Revolt Desktop and is *not* an issue with Revolt Desktop itself.
required: false

View file

@ -1,6 +1,6 @@
name: Feature request
description: Make a feature request
title: "[Feature Request]"
title: "feature request: "
labels: ["enhancement"]
body:
- 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
/.idea
.yarn/cache
.yarn/install-state.gz
public/assets
public/assets_*
!public/assets_default

View file

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

View file

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

View file

@ -1,14 +1,15 @@
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
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 { AppContext } from "../../context/revoltjs/RevoltClient";
import { ImageIconBase, IconBaseProps } from "./IconBase";
import fallback from "./assets/group.png";
import { ImageIconBase, IconBaseProps } from "./IconBase";
interface Props extends IconBaseProps<Channel> {
isServerChannel?: boolean;
}
@ -32,7 +33,7 @@ export default observer(
...imgProps
} = props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
target?.icon ?? attachment ?? undefined,
{ max_side: 256 },
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 { Ref } from "preact";
@ -6,7 +7,7 @@ import { Ref } from "preact";
export interface IconBaseProps<T> {
target?: T;
url?: string;
attachment?: Attachment;
attachment?: Nullable<API.File>;
size: number;
hover?: boolean;

View file

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

View file

@ -1,5 +1,5 @@
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 { useContext } from "preact/hooks";
@ -39,7 +39,7 @@ export default observer(
const { target, attachment, size, animate, server_name, ...imgProps } =
props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
target?.icon ?? attachment ?? undefined,
{ max_side: 256 },
animate,
);

View file

@ -1,5 +1,5 @@
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 { memo } from "preact/compat";

View file

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

View file

@ -11,8 +11,7 @@ import {
MessageSquareEdit,
} from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { SystemMessage as SystemMessageI } from "revolt-api/types/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Message, API } from "revolt.js";
import styled from "styled-components/macro";
import { useTriggerEvents } from "preact-context-menu";
@ -75,13 +74,11 @@ export const SystemMessage = observer(
({ attachContext, message, highlight, hideInfo }: Props) => {
const data = message.asSystemMessage;
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) {
case "text":
children = <span>{data.content}</span>;
break;
case "user_added":
case "user_remove":
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 classNames from "classnames";
@ -14,7 +14,7 @@ import Spoiler from "./Spoiler";
import TextFile from "./TextFile";
interface Props {
attachment: AttachmentI;
attachment: API.File;
hasContent?: boolean;
}

View file

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

View file

@ -9,6 +9,7 @@ const Grid = styled.div<{ width: number; height: number }>`
--height: ${(props) => props.height}px;
display: grid;
overflow: hidden;
aspect-ratio: ${(props) => props.width} / ${(props) => props.height};
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 classNames from "classnames";
@ -10,12 +10,12 @@ import { AppContext } from "../../../../context/revoltjs/RevoltClient";
enum ImageLoadingState {
Loading,
Loaded,
Error
Error,
}
type Props = JSX.HTMLAttributes<HTMLImageElement> & {
attachment: Attachment;
}
attachment: API.File;
};
export default function ImageFile({ attachment, ...props }: Props) {
const [loading, setLoading] = useState(ImageLoadingState.Loading);
@ -23,25 +23,19 @@ export default function ImageFile({ attachment, ...props }: Props) {
const { openScreen } = useIntermediate();
const url = client.generateFileURL(attachment)!;
return <img
{...props}
src={url}
alt={attachment.filename}
loading="lazy"
className={classNames(styles.image, {
[styles.loading]: loading !== ImageLoadingState.Loaded
})}
onClick={() =>
openScreen({ id: "image_viewer", attachment })
}
onMouseDown={(ev) =>
ev.button === 1 && window.open(url, "_blank")
}
onLoad={() =>
setLoading(ImageLoadingState.Loaded)
}
onError={() =>
setLoading(ImageLoadingState.Error)
}
/>
return (
<img
{...props}
src={url}
alt={attachment.filename}
loading="lazy"
className={classNames(styles.image, {
[styles.loading]: loading !== ImageLoadingState.Loaded,
})}
onClick={() => openScreen({ id: "image_viewer", attachment })}
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
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 { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Channel, Message, API } from "revolt.js";
import styled, { css } from "styled-components/macro";
import { Text } from "preact-i18n";
@ -174,7 +172,7 @@ export const MessageReply = observer(
<ReplyBase head={index === 0}>
{/*<Reply size={16} />*/}
{message.author?.relationship === RelationshipStatus.Blocked ? (
{message.author?.relationship === "Blocked" ? (
<Text id="app.main.channel.misc.blocked_user" />
) : (
<>
@ -225,9 +223,10 @@ export const MessageReply = observer(
)}
<Markdown
disallowBigEmoji
content={(
message.content as string
).replace(/\n/g, " ")}
content={message.content?.replace(
/\n/g,
" ",
)}
/>
</div>
</>

View file

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

View file

@ -1,6 +1,6 @@
import { DownArrowAlt } from "@styled-icons/boxicons-regular";
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 { Text } from "preact-i18n";

View file

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

View file

@ -1,7 +1,7 @@
import { UpArrowAlt } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Channel } from "revolt.js";
import { decodeTime } from "ulid";
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 { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Channel, Message } from "revolt.js";
import styled from "styled-components/macro";
import { Text } from "preact-i18n";
@ -188,9 +187,10 @@ export default observer(({ channel, replies, setReplies }: Props) => {
) : (
<Markdown
disallowBigEmoji
content={(
message.content as string
).replace(/\n/g, " ")}
content={message.content?.replace(
/\n/g,
" ",
)}
/>
)}
</div>

View file

@ -1,6 +1,5 @@
import { observer } from "mobx-react-lite";
import { RelationshipStatus } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Channel } from "revolt.js";
import styled from "styled-components/macro";
import { Text } from "preact-i18n";
@ -65,7 +64,7 @@ export default observer(({ channel }: Props) => {
(x) =>
typeof x !== "undefined" &&
x._id !== x.client.user!._id &&
x.relationship !== RelationshipStatus.Blocked,
x.relationship !== "Blocked",
);
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 classNames from "classnames";
@ -13,7 +13,7 @@ import Attachment from "../attachments/Attachment";
import EmbedMedia from "./EmbedMedia";
interface Props {
embed: EmbedI;
embed: API.Embed;
}
const MAX_EMBED_WIDTH = 480;
@ -68,7 +68,8 @@ export default function Embed({ embed }: Props) {
mh = embed.video?.height ?? 720;
break;
}
case "Twitch": {
case "Twitch":
case "Lightspeed": {
mw = 1280;
mh = 720;
break;
@ -89,6 +90,20 @@ export default function Embed({ embed }: Props) {
}
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 (
<div
className={classNames(styles.embed, styles.website)}
@ -128,7 +143,7 @@ export default function Embed({ embed }: Props) {
<a
onMouseDown={(ev) =>
(ev.button === 0 || ev.button === 1) &&
openLink(embed.url)
openLink(embed.url!)
}
className={styles.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:
return null;
}

View file

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

View file

@ -1,5 +1,5 @@
/* 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";
@ -7,7 +7,7 @@ import { useIntermediate } from "../../../../context/intermediate/Intermediate";
import { useClient } from "../../../../context/revoltjs/RevoltClient";
interface Props {
embed: JanuaryEmbed;
embed: API.Embed;
width?: number;
height: number;
}
@ -47,6 +47,17 @@ export default function EmbedMedia({ embed, width, height }: Props) {
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":
return (
<iframe
@ -83,7 +94,21 @@ export default function EmbedMedia({ embed, width, height }: Props) {
);
}
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;
return (
<img
@ -94,7 +119,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
onClick={() =>
openScreen({
id: "image_viewer",
embed: embed.image,
embed: embed.image!,
})
}
onMouseDown={(ev) =>

View file

@ -1,12 +1,12 @@
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 IconButton from "../../../ui/IconButton";
interface Props {
embed: EmbedImage;
embed: API.Image;
}
export default function EmbedMediaActions({ embed }: Props) {

View file

@ -1,11 +1,23 @@
import { Shield } from "@styled-icons/boxicons-regular";
import { Badges } from "revolt-api/types/Users";
import styled from "styled-components/macro";
import { Localizer, Text } from "preact-i18n";
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`
gap: 8px;
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";

View file

@ -1,7 +1,7 @@
import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
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 { 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 { Children } from "../../../types/Preact";

View file

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

View file

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

View file

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

View file

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

View file

@ -5,10 +5,6 @@ import "katex/dist/katex.min.css";
import MarkdownIt from "markdown-it";
// @ts-expect-error No typings.
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
// @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 styles from "./Markdown.module.scss";
@ -64,8 +60,6 @@ export const md: MarkdownIt = MarkdownIt({
.disable("image")
.use(MarkdownEmoji, { defs: emojiDictionary })
.use(MarkdownSpoilers)
.use(MarkdownSup)
.use(MarkdownSub)
.use(MarkdownKatex, {
throwOnError: false,
maxExpand: 0,
@ -129,7 +123,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const { openLink } = useIntermediate();
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 don't care if the mention changes.

View file

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

View file

@ -4,12 +4,14 @@ import { useContext } from "preact/hooks";
import {
ClientStatus,
StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient";
import Banner from "../../ui/Banner";
export default function ConnectionStatus() {
const status = useContext(StatusContext);
const client = useClient();
if (status === ClientStatus.OFFLINE) {
return (
@ -20,7 +22,10 @@ export default function ConnectionStatus() {
} else if (status === ClientStatus.DISCONNECTED) {
return (
<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>
);
} else if (status === ClientStatus.CONNECTING) {

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite";
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 { Ref } from "preact";

View file

@ -1,6 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
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";

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 { 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;
contrast?: boolean;
className?: string;
children: Children;
children?: Children;
description?: Children;
onChange: (state: boolean) => void;
}

View file

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

View file

@ -1,13 +1,6 @@
import { Prompt } from "react-router";
import { useHistory } from "react-router-dom";
import type { Attachment } from "revolt-api/types/Autumn";
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 { API, Channel, Message, Server, User } from "revolt.js";
import { createContext } from "preact";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
@ -31,6 +24,7 @@ export type Screen =
| { id: "clipboard"; text: string }
| { id: "token_reveal"; token: string; username: string }
| { id: "external_link_prompt"; link: string }
| { id: "sessions", confirm: () => void }
| {
id: "_prompt";
question: Children;
@ -61,7 +55,11 @@ export type Screen =
| {
type: "create_channel";
target: Server;
cb?: (channel: TextChannel | VoiceChannel) => void;
cb?: (
channel: Channel & {
channel_type: "TextChannel" | "VoiceChannel";
},
) => void;
}
| { type: "create_category"; target: Server }
))
@ -101,11 +99,11 @@ export type Screen =
omit?: string[];
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: "pending_requests"; users: User[] }
| { 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";
server: Server;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 { useContext, useState } from "preact/hooks";
@ -13,7 +13,7 @@ import { takeError } from "../../revoltjs/util";
interface Props {
onClose: () => void;
onCreate: (bot: Bot) => void;
onCreate: (bot: API.Bot) => void;
}
interface FormInputs {

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 { Text } from "preact-i18n";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -50,7 +50,7 @@ export default observer(({ children }: Props) => {
useEffect(() => {
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()]);
useEffect(() => registerEvents(state.auth, setStatus, client), [client]);
useEffect(() => registerEvents(state, setStatus, client), [client]);
if (!loaded || status === ClientStatus.LOADING) {
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.
*/
import { Message } from "revolt.js/dist/maps/Messages";
import { Message } from "revolt.js";
import { useContext, useEffect } from "preact/hooks";

View file

@ -1,7 +1,7 @@
/**
* 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";

View file

@ -1,14 +1,16 @@
import { Client } from "revolt.js/dist";
import { Client, Server } from "revolt.js";
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 { ClientStatus } from "./RevoltClient";
export function registerEvents(
auth: Auth,
state: State,
setStatus: StateUpdater<ClientStatus>,
client: Client,
) {
@ -25,9 +27,22 @@ export function registerEvents(
},
logout: () => {
auth.logout();
state.auth.logout();
state.reset();
setStatus(ClientStatus.READY);
},
"channel/delete": (channel_id: string) => {
deleteRenderer(channel_id);
},
"server/delete": (_, server: Server) => {
if (server) {
for (const channel_id of server.channel_ids) {
deleteRenderer(channel_id);
}
}
},
};
if (import.meta.env.DEV) {

View file

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

1
src/globals.d.ts vendored
View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { action, makeAutoObservable } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { Nullable } from "revolt.js/dist/util/null";
import { Channel } from "revolt.js";
import { Message } from "revolt.js";
import { Nullable } from "revolt.js";
import { SimpleRenderer } from "./simple/SimpleRenderer";
import { RendererRoutines, ScrollState } from "./types";
@ -222,3 +222,7 @@ export function getRenderer(channel: Channel) {
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";

View file

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

View file

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

View file

@ -1,7 +1,7 @@
// @ts-expect-error No typings.
import stringify from "json-stringify-deterministic";
import localforage from "localforage";
import { makeAutoObservable, reaction } from "mobx";
import { makeAutoObservable, reaction, runInAction } from "mobx";
import { Client } from "revolt.js";
import { reportError } from "../lib/ErrorBoundary";
@ -184,10 +184,12 @@ export default class State {
}
if (Object.keys(obj).length > 0) {
client.syncSetSettings(
obj as any,
revision,
);
if (client.websocket.connected) {
client.syncSetSettings(
obj as any,
revision,
);
}
}
break;
}
@ -198,12 +200,14 @@ export default class State {
}
this.sync.setRevision(id, revision);
client.syncSetSettings(
(
store as unknown as Syncable
).toSyncable(),
revision,
);
if (client.websocket.connected) {
client.syncSetSettings(
(
store as unknown as Syncable
).toSyncable(),
revision,
);
}
}
}
}
@ -263,6 +267,26 @@ export default class State {
// Post-hydration, init plugins.
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;

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