merge: branch 'quark/permissions'

This commit is contained in:
Paul Makles 2022-04-29 13:48:38 +01:00
commit 37d5ba24c5
117 changed files with 10609 additions and 6253 deletions

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

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

9
.yarnrc.yml Normal file
View file

@ -0,0 +1,9 @@
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"
yarnPath: .yarn/releases/yarn-3.2.0.cjs

2
external/lang vendored

@ -1 +1 @@
Subproject commit 6a72c5c952eedfbeb8a193a8a4b97927cc44cd6f Subproject commit 50a710d761330632716b3f6d17ed964465d79213

View file

@ -61,7 +61,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",
@ -91,16 +90,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.28",
"@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",
@ -118,6 +117,7 @@
"@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,14 @@
"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-sub": "^1.0.0",
"markdown-it-sup": "^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",
@ -147,8 +148,7 @@
"react-scroll": "^1.8.2", "react-scroll": "^1.8.2",
"react-virtualized-auto-sizer": "^1.0.5", "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-rc.21",
"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 +160,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

@ -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,7 @@ 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 { 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 +17,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) {

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 (
<img
{...props} {...props}
src={url} src={url}
alt={attachment.filename} alt={attachment.filename}
loading="lazy" loading="lazy"
className={classNames(styles.image, { className={classNames(styles.image, {
[styles.loading]: loading !== ImageLoadingState.Loaded [styles.loading]: loading !== ImageLoadingState.Loaded,
})} })}
onClick={() => onClick={() => openScreen({ id: "image_viewer", attachment })}
openScreen({ id: "image_viewer", attachment }) onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
} onLoad={() => setLoading(ImageLoadingState.Loaded)}
onMouseDown={(ev) => onError={() => setLoading(ImageLoadingState.Error)}
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 { 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" />
) : ( ) : (
<> <>

View file

@ -1,5 +1,5 @@
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 { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
@ -13,7 +13,7 @@ import {
import Preloader from "../../../ui/Preloader"; import Preloader from "../../../ui/Preloader";
interface Props { interface Props {
attachment: Attachment; attachment: API.File;
} }
const fileCache: { [key: string]: string } = {}; const fileCache: { [key: string]: string } = {};

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";

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;
@ -128,7 +128,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}

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 () => {
try {
setProcessing(true); setProcessing(true);
if (invite.type === "Server") { try {
if (client.servers.get(invite.server_id)) { await client.joinInvite(invite);
history.push( history.push(
`/server/${invite.server_id}/channel/${invite.channel_id}`, `/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;
} }
@ -94,7 +94,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");
@ -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>;
@ -120,7 +118,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

@ -129,7 +129,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) {

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

View file

@ -0,0 +1,124 @@
import Long from "long";
import { API } from "revolt.js";
import { Permission } from "revolt.js";
import styled 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;
permission: number;
value: API.OverrideField | number;
onChange: (value: API.OverrideField | number) => void;
}
type State = "Allow" | "Neutral" | "Deny";
const PermissionEntry = styled.label`
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;
}
.description {
font-size: 0.8em;
color: var(--secondary-foreground);
}
`;
export function PermissionSelect({
id,
permission,
value,
onChange,
}: 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(),
});
}
return (
<PermissionEntry>
<span class="title">
<Text id={`permissions.${id}.t`}>{id}</Text>
<span class="description">
<Text id={`permissions.${id}.d`} />
</span>
</span>
{typeof value === "object" ? (
<OverrideSwitch state={state} onChange={onSwitch} />
) : (
<Checkbox
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";

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";
@ -61,7 +54,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 +98,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

@ -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

@ -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

@ -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,8 +216,7 @@ export const UserProfile = observer(
</Tooltip> </Tooltip>
</Localizer> </Localizer>
)} )}
{user.relationship === RelationshipStatus.User && {user.relationship === "User" && !dummy && (
!dummy && (
<IconButton <IconButton
onClick={() => { onClick={() => {
onClose?.(); onClose?.();
@ -227,15 +228,13 @@ export const UserProfile = observer(
{!user.bot && {!user.bot &&
flags != 2 && flags != 2 &&
flags != 4 && flags != 4 &&
(user.relationship === (user.relationship === "Incoming" ||
RelationshipStatus.Incoming || user.relationship === "None") && (
user.relationship ===
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 +246,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"}

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,23 @@ 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) { switch (status) {
if ( case 429:
error?.response?.status === 401 || return "TooManyRequests";
error?.response?.status === 403 case 401:
) { case 403:
return "Unauthorized"; return "Unauthorized";
} else if (error && !!error.isAxiosError && !error.response) { default:
return error.response.type ?? "UnknownError";
}
} else if (error.request) {
return "NetworkError"; return "NetworkError";
} }
console.error(error); console.error(error);
return "UnknownError"; return "UnknownError";
}
return id;
} }
export function getChannelName( export function getChannelName(

View file

@ -12,17 +12,8 @@ import {
} 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 { 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 +51,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 +73,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 +88,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 }
@ -505,9 +496,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 +507,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,
@ -592,29 +583,29 @@ export default function ContextMenus() {
if (!user.bot) { if (!user.bot) {
let actions: Action["action"][]; let actions: Action["action"][];
switch (user.relationship) { switch (user.relationship) {
case RelationshipStatus.User: case "User":
actions = []; actions = [];
break; break;
case RelationshipStatus.Friend: case "Friend":
actions = ["remove_friend", "block_user"]; actions = ["remove_friend", "block_user"];
break; break;
case RelationshipStatus.Incoming: case "Incoming":
actions = [ actions = [
"add_friend", "add_friend",
"cancel_friend", "cancel_friend",
"block_user", "block_user",
]; ];
break; break;
case RelationshipStatus.Outgoing: case "Outgoing":
actions = ["cancel_friend", "block_user"]; actions = ["cancel_friend", "block_user"];
break; break;
case RelationshipStatus.Blocked: case "Blocked":
actions = ["unblock_user"]; actions = ["unblock_user"];
break; break;
case RelationshipStatus.BlockedOther: case "BlockedOther":
actions = ["block_user"]; actions = ["block_user"];
break; break;
case RelationshipStatus.None: case "None":
default: default:
if ( if (
(user.flags && 2) || (user.flags && 2) ||
@ -673,9 +664,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 +677,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 +707,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 +747,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 +890,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 +900,7 @@ export default function ContextMenus() {
if ( if (
serverPermissions & serverPermissions &
ServerPermission.ManageServer Permission.ManageServer
) )
generateAction( generateAction(
{ {
@ -926,7 +913,7 @@ export default function ContextMenus() {
if ( if (
serverPermissions & serverPermissions &
ServerPermission.ManageChannels Permission.ManageChannel
) )
generateAction({ generateAction({
action: "delete_channel", action: "delete_channel",
@ -958,20 +945,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 +1042,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 +1051,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 +1060,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 +1069,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

@ -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,11 +184,13 @@ export default class State {
} }
if (Object.keys(obj).length > 0) { if (Object.keys(obj).length > 0) {
if (client.websocket.connected) {
client.syncSetSettings( client.syncSetSettings(
obj as any, obj as any,
revision, revision,
); );
} }
}
break; break;
} }
default: { default: {
@ -198,6 +200,7 @@ export default class State {
} }
this.sync.setRevision(id, revision); this.sync.setRevision(id, revision);
if (client.websocket.connected) {
client.syncSetSettings( client.syncSetSettings(
( (
store as unknown as Syncable store as unknown as Syncable
@ -207,6 +210,7 @@ export default class State {
} }
} }
} }
}
} catch (err) { } catch (err) {
console.error("Failed to serialise!"); console.error("Failed to serialise!");
console.error(err); console.error(err);
@ -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;

View file

@ -1,5 +1,5 @@
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import { Session } from "revolt-api/types/Auth"; import { API } from "revolt.js";
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme"; import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
@ -58,7 +58,7 @@ export interface LegacySyncOptions {
export interface LegacyAuthState { export interface LegacyAuthState {
accounts: { accounts: {
[key: string]: { [key: string]: {
session: Session; session: API.Session;
}; };
}; };
active?: string; active?: string;

View file

@ -1,6 +1,6 @@
import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
import { Session } from "revolt-api/types/Auth"; import { API } from "revolt.js";
import { Nullable } from "revolt.js/dist/util/null"; import { Nullable } from "revolt.js";
import { mapToRecord } from "../../lib/conversion"; import { mapToRecord } from "../../lib/conversion";
@ -8,7 +8,7 @@ import Persistent from "../interfaces/Persistent";
import Store from "../interfaces/Store"; import Store from "../interfaces/Store";
interface Account { interface Account {
session: Session; session: API.Session;
} }
export interface Data { export interface Data {
@ -82,7 +82,7 @@ export default class Auth implements Store, Persistent<Data> {
* Add a new session to the auth manager. * Add a new session to the auth manager.
* @param session Session * @param session Session
*/ */
@action setSession(session: Session) { @action setSession(session: API.Session) {
this.sessions.set(session.user_id, { session }); this.sessions.set(session.user_id, { session });
this.current = session.user_id; this.current = session.user_id;
} }

View file

@ -1,8 +1,7 @@
import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
import { Presence, RelationshipStatus } from "revolt-api/types/Users"; import { Channel } from "revolt.js";
import { Channel } from "revolt.js/dist/maps/Channels"; import { Message } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages"; import { Server } from "revolt.js";
import { Server } from "revolt.js/dist/maps/Servers";
import { mapToRecord } from "../../lib/conversion"; import { mapToRecord } from "../../lib/conversion";
@ -113,7 +112,7 @@ export default class NotificationOptions
*/ */
shouldNotify(message: Message) { shouldNotify(message: Message) {
// Make sure the author is not blocked. // Make sure the author is not blocked.
if (message.author?.relationship === RelationshipStatus.Blocked) { if (message.author?.relationship === "Blocked") {
return false; return false;
} }
@ -124,7 +123,7 @@ export default class NotificationOptions
} }
// Check whether we are busy. // Check whether we are busy.
if (user.status?.presence === Presence.Busy) { if (user.status?.presence === "Busy") {
return false; return false;
} }

View file

@ -1,7 +1,7 @@
import { action, computed, makeAutoObservable } from "mobx"; import { action, computed, makeAutoObservable } from "mobx";
import { RevoltConfiguration } from "revolt-api/types/Core"; import { API } from "revolt.js";
import { Client } from "revolt.js"; import { Client } from "revolt.js";
import { Nullable } from "revolt.js/dist/util/null"; import { Nullable } from "revolt.js";
import { isDebug } from "../../revision"; import { isDebug } from "../../revision";
import Persistent from "../interfaces/Persistent"; import Persistent from "../interfaces/Persistent";
@ -11,9 +11,9 @@ import Store from "../interfaces/Store";
* Stores server configuration data. * Stores server configuration data.
*/ */
export default class ServerConfig export default class ServerConfig
implements Store, Persistent<RevoltConfiguration> implements Store, Persistent<API.RevoltConfig>
{ {
private config: Nullable<RevoltConfiguration>; private config: Nullable<API.RevoltConfig>;
/** /**
* Construct new ServerConfig store. * Construct new ServerConfig store.
@ -32,7 +32,7 @@ export default class ServerConfig
return JSON.parse(JSON.stringify(this.config)); return JSON.parse(JSON.stringify(this.config));
} }
@action hydrate(data: RevoltConfiguration) { @action hydrate(data: API.RevoltConfig) {
this.config = data ?? null; this.config = data ?? null;
} }
@ -68,7 +68,7 @@ export default class ServerConfig
* Set server configuration. * Set server configuration.
* @param config Server configuration * @param config Server configuration
*/ */
@action set(config: RevoltConfiguration) { @action set(config: API.RevoltConfig) {
this.config = config; this.config = config;
} }
} }

View file

@ -7,7 +7,6 @@ import {
runInAction, runInAction,
} from "mobx"; } from "mobx";
import { Client } from "revolt.js"; import { Client } from "revolt.js";
import { UserSettings } from "revolt-api/types/Sync";
import { mapToRecord } from "../../lib/conversion"; import { mapToRecord } from "../../lib/conversion";
@ -104,7 +103,7 @@ export default class Sync implements Store, Persistent<Data> {
return this.revision.get(key); return this.revision.get(key);
} }
@action apply(data: UserSettings) { @action apply(data: Record<string, [number, string]>) {
const tryRead = (key: string) => { const tryRead = (key: string) => {
if (key in data) { if (key in data) {
const revision = data[key][0]; const revision = data[key][0];

View file

@ -1,3 +1,4 @@
// @ts-expect-error No typings.
import rgba from "color-rgba"; import rgba from "color-rgba";
import { makeAutoObservable, computed, action } from "mobx"; import { makeAutoObservable, computed, action } from "mobx";

View file

@ -3,7 +3,7 @@ import { Ghost } from "@styled-icons/boxicons-solid";
import { reaction } from "mobx"; import { reaction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router-dom"; import { Redirect, useParams } from "react-router-dom";
import { Channel as ChannelI } from "revolt.js/dist/maps/Channels"; import { Channel as ChannelI } 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";
@ -97,11 +97,12 @@ const PlaceholderBase = styled.div`
} }
`; `;
export function Channel({ id, server_id }: { id: string; server_id: string }) { export const Channel = observer(
({ id, server_id }: { id: string; server_id: string }) => {
const client = useClient(); const client = useClient();
const channel = client.channels.get(id);
if (server_id && !channel) { if (!client.channels.exists(id)) {
if (server_id) {
const server = client.servers.get(server_id); const server = client.servers.get(server_id);
if (server && server.channel_ids.length > 0) { if (server && server.channel_ids.length > 0) {
return ( return (
@ -110,16 +111,21 @@ export function Channel({ id, server_id }: { id: string; server_id: string }) {
/> />
); );
} }
} else {
return <Redirect to="/" />;
} }
if (!channel) return <ChannelPlaceholder />; return <ChannelPlaceholder />;
}
const channel = client.channels.get(id)!;
if (channel.channel_type === "VoiceChannel") { if (channel.channel_type === "VoiceChannel") {
return <VoiceChannel channel={channel} />; return <VoiceChannel channel={channel} />;
} }
return <TextChannel channel={channel} />; return <TextChannel channel={channel} />;
} },
);
const TextChannel = observer(({ channel }: { channel: ChannelI }) => { const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
const layout = useApplicationState().layout; const layout = useApplicationState().layout;
@ -143,9 +149,9 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
// Mark channel as read. // Mark channel as read.
useEffect(() => { useEffect(() => {
setLastId( setLastId(
channel.unread (channel.unread
? channel.client.unreads?.getUnread(channel._id)?.last_id ? channel.client.unreads?.getUnread(channel._id)?.last_id
: undefined ?? undefined, : undefined) ?? undefined,
); );
const checkUnread = () => const checkUnread = () =>

View file

@ -6,8 +6,8 @@ import {
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { Notepad, Group } from "@styled-icons/boxicons-solid"; import { Notepad, Group } 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 } from "revolt.js";
import { User } from "revolt.js/dist/maps/Users"; import { User } from "revolt.js";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components/macro";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";

View file

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

View file

@ -2,7 +2,7 @@ import { runInAction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { animateScroll } from "react-scroll"; import { animateScroll } from "react-scroll";
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 useResizeObserver from "use-resize-observer"; import useResizeObserver from "use-resize-observer";

View file

@ -1,4 +1,4 @@
import { Message } from "revolt.js/dist/maps/Messages"; import { Message } from "revolt.js";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";

View file

@ -2,10 +2,9 @@
import { X } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Masquerade } from "revolt-api/types/Channels"; import { API } from "revolt.js";
import { RelationshipStatus } from "revolt-api/types/Users"; import { Message as MessageI } from "revolt.js";
import { Message as MessageI } from "revolt.js/dist/maps/Messages"; import { Nullable } from "revolt.js";
import { Nullable } from "revolt.js/dist/util/null";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
@ -99,10 +98,10 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
function compare( function compare(
current: string, current: string,
curAuthor: string, curAuthor: string,
currentMasq: Nullable<Masquerade>, currentMasq: Nullable<API.Masquerade>,
previous: string, previous: string,
prevAuthor: string, prevAuthor: string,
previousMasq: Nullable<Masquerade>, previousMasq: Nullable<API.Masquerade>,
) { ) {
head = false; head = false;
const atime = decodeTime(current), const atime = decodeTime(current),
@ -172,9 +171,7 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
highlight={highlight === message._id} highlight={highlight === message._id}
/>, />,
); );
} else if ( } else if (message.author?.relationship === "Blocked") {
message.author?.relationship === RelationshipStatus.Blocked
) {
blocked++; blocked++;
} else { } else {
if (blocked > 0) pushBlocked(); if (blocked > 0) pushBlocked();

View file

@ -2,8 +2,7 @@ import { X, Plus } from "@styled-icons/boxicons-regular";
import { PhoneCall, Envelope, UserX } from "@styled-icons/boxicons-solid"; import { PhoneCall, Envelope, UserX } 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 { User } from "revolt.js";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Friend.module.scss"; import styles from "./Friend.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@ -33,7 +32,7 @@ export const Friend = observer(({ user }: Props) => {
const actions: Children[] = []; const actions: Children[] = [];
let subtext: Children = null; let subtext: Children = null;
if (user.relationship === RelationshipStatus.Friend) { if (user.relationship === "Friend") {
subtext = <UserStatus user={user} />; subtext = <UserStatus user={user} />;
actions.push( actions.push(
<> <>
@ -70,7 +69,7 @@ export const Friend = observer(({ user }: Props) => {
); );
} }
if (user.relationship === RelationshipStatus.Incoming) { if (user.relationship === "Incoming") {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"
@ -83,14 +82,14 @@ export const Friend = observer(({ user }: Props) => {
subtext = <Text id="app.special.friends.incoming" />; subtext = <Text id="app.special.friends.incoming" />;
} }
if (user.relationship === RelationshipStatus.Outgoing) { if (user.relationship === "Outgoing") {
subtext = <Text id="app.special.friends.outgoing" />; subtext = <Text id="app.special.friends.outgoing" />;
} }
if ( if (
user.relationship === RelationshipStatus.Friend || user.relationship === "Friend" ||
user.relationship === RelationshipStatus.Outgoing || user.relationship === "Outgoing" ||
user.relationship === RelationshipStatus.Incoming user.relationship === "Incoming"
) { ) {
actions.push( actions.push(
<IconButton <IconButton
@ -103,7 +102,7 @@ export const Friend = observer(({ user }: Props) => {
onClick={(ev) => onClick={(ev) =>
stopPropagation( stopPropagation(
ev, ev,
user.relationship === RelationshipStatus.Friend user.relationship === "Friend"
? openScreen({ ? openScreen({
id: "special_prompt", id: "special_prompt",
type: "unfriend_user", type: "unfriend_user",
@ -117,7 +116,7 @@ export const Friend = observer(({ user }: Props) => {
); );
} }
if (user.relationship === RelationshipStatus.Blocked) { if (user.relationship === "Blocked") {
actions.push( actions.push(
<IconButton <IconButton
type="circle" type="circle"

View file

@ -1,9 +1,7 @@
import { ChevronRight } from "@styled-icons/boxicons-regular"; import { ChevronRight } from "@styled-icons/boxicons-regular";
import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid"; import { UserDetail, MessageAdd, UserPlus } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { RelationshipStatus, Presence } from "revolt-api/types/Users"; import { User } from "revolt.js";
import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components/macro";
import styles from "./Friend.module.scss"; import styles from "./Friend.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@ -31,36 +29,32 @@ export default observer(() => {
const users = [...client.users.values()]; const users = [...client.users.values()];
users.sort((a, b) => a.username.localeCompare(b.username)); users.sort((a, b) => a.username.localeCompare(b.username));
const friends = users.filter( const friends = users.filter((x) => x.relationship === "Friend");
(x) => x.relationship === RelationshipStatus.Friend,
);
const lists = [ const lists = [
[ ["", users.filter((x) => x.relationship === "Incoming")],
"",
users.filter((x) => x.relationship === RelationshipStatus.Incoming),
],
[ [
"app.special.friends.sent", "app.special.friends.sent",
users.filter((x) => x.relationship === RelationshipStatus.Outgoing), users.filter((x) => x.relationship === "Outgoing"),
"outgoing", "outgoing",
], ],
[ [
"app.status.online", "app.status.online",
friends.filter( friends.filter(
(x) => x.online && x.status?.presence !== Presence.Invisible, (x) => x.online && x.status?.presence !== "Invisible",
), ),
"online", "online",
], ],
[ [
"app.status.offline", "app.status.offline",
friends.filter( friends.filter(
(x) => !x.online || x.status?.presence === Presence.Invisible, (x) => !x.online || x.status?.presence === "Invisible",
), ),
"offline", "offline",
], ],
[ [
"app.special.friends.blocked", "app.special.friends.blocked",
users.filter((x) => x.relationship === RelationshipStatus.Blocked), users.filter((x) => x.relationship === "Blocked"),
"blocked", "blocked",
], ],
] as [string, User[], string][]; ] as [string, User[], string][];

View file

@ -21,9 +21,9 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../mobx/State"; import { useApplicationState } from "../../mobx/State";
import { useIntermediate } from "../../context/intermediate/Intermediate";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
import Tooltip from "../../components/common/Tooltip";
import { PageHeader } from "../../components/ui/Header"; import { PageHeader } from "../../components/ui/Header";
import CategoryButton from "../../components/ui/fluent/CategoryButton"; import CategoryButton from "../../components/ui/fluent/CategoryButton";
import wideSVG from "/assets/wide.svg"; import wideSVG from "/assets/wide.svg";
@ -42,6 +42,7 @@ const Overlay = styled.div`
`; `;
export default observer(() => { export default observer(() => {
const { openScreen } = useIntermediate();
const client = useContext(AppContext); const client = useContext(AppContext);
const state = useApplicationState(); const state = useApplicationState();
@ -93,7 +94,13 @@ export default observer(() => {
<img src={wideSVG} /> <img src={wideSVG} />
</h3> </h3>
<div className={styles.actions}> <div className={styles.actions}>
<Link to="/settings"> <a
onClick={() =>
openScreen({
id: "special_input",
type: "create_group",
})
}>
<CategoryButton <CategoryButton
action="chevron" action="chevron"
icon={<PlusCircle size={32} />} icon={<PlusCircle size={32} />}
@ -102,7 +109,7 @@ export default observer(() => {
}> }>
<Text id="app.home.group" /> <Text id="app.home.group" />
</CategoryButton> </CategoryButton>
</Link> </a>
<Link to="/discover"> <Link to="/discover">
<a> <a>
<CategoryButton <CategoryButton

View file

@ -1,7 +1,7 @@
import { ArrowBack } from "@styled-icons/boxicons-regular"; import { ArrowBack } from "@styled-icons/boxicons-regular";
import { autorun } from "mobx"; import { autorun } from "mobx";
import { Redirect, useHistory, useParams } from "react-router-dom"; import { Redirect, useHistory, useParams } from "react-router-dom";
import { RetrievedInvite } from "revolt-api/types/Invites"; import { API } from "revolt.js";
import styles from "./Invite.module.scss"; import styles from "./Invite.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -36,7 +36,7 @@ export default function Invite() {
const { code } = useParams<{ code: string }>(); const { code } = useParams<{ code: string }>();
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 [invite, setInvite] = useState<RetrievedInvite | undefined>( const [invite, setInvite] = useState<API.InviteResponse | undefined>(
undefined, undefined,
); );
@ -92,6 +92,8 @@ export default function Invite() {
); );
} }
if (invite.type === "Group") return <h1>unimplemented</h1>;
return ( return (
<div <div
className={styles.invite} className={styles.invite}
@ -156,42 +158,17 @@ export default function Invite() {
return history.push("/"); return history.push("/");
} }
try {
setProcessing(true); setProcessing(true);
if (invite.type === "Server") { try {
if ( await client.joinInvite(invite);
client.servers.get(invite.server_id)
) {
history.push( history.push(
`/server/${invite.server_id}/channel/${invite.channel_id}`, `/server/${invite.server_id}/channel/${invite.channel_id}`,
); );
}
const dispose = autorun(() => {
const server = client.servers.get(
invite.server_id,
);
defer(() => {
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) {
setError(takeError(err)); setError(takeError(err));
} finally {
setProcessing(false); setProcessing(false);
} }
}}> }}>

View file

@ -1,6 +1,5 @@
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { ServerPermission } from "revolt.js"; import { API, Permission } from "revolt.js";
import { Route } from "revolt.js/dist/api/routes";
import styled from "styled-components/macro"; import styled from "styled-components/macro";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
@ -37,8 +36,7 @@ const Option = styled.div`
export default function InviteBot() { export default function InviteBot() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const client = useClient(); const client = useClient();
const [data, setData] = const [data, setData] = useState<API.PublicBot>();
useState<Route<"GET", "/bots/id/invite">["response"]>();
useEffect(() => { useEffect(() => {
client.bots.fetchPublic(id).then(setData); client.bots.fetchPublic(id).then(setData);
@ -72,11 +70,7 @@ export default function InviteBot() {
onChange={(e) => setServer(e.currentTarget.value)}> onChange={(e) => setServer(e.currentTarget.value)}>
<option value="none">Select a server</option> <option value="none">Select a server</option>
{[...client.servers.values()] {[...client.servers.values()]
.filter( .filter((x) => x.havePermission("ManageServer"))
(x) =>
x.permission &
ServerPermission.ManageServer,
)
.map((server) => ( .map((server) => (
<option value={server._id} key={server._id}> <option value={server._id} key={server._id}>
{server.name} {server.name}

View file

@ -1,6 +1,5 @@
import { detect } from "detect-browser"; import { detect } from "detect-browser";
import { Session } from "revolt-api/types/Auth"; import { API } from "revolt.js";
import { Client } from "revolt.js";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
@ -44,25 +43,26 @@ export function FormLogin() {
// This should be replaced in the future. // This should be replaced in the future.
const client = state.config.createClient(); const client = state.config.createClient();
await client.fetchConfiguration(); await client.fetchConfiguration();
const session = (await client.req( const session = await client.api.post("/auth/session/login", {
"POST", ...data,
"/auth/session/login", friendly_name,
{ ...data, friendly_name }, });
)) as unknown as Session;
client.session = session; if (session.result !== "Success") {
(client as any).Axios.defaults.headers = { alert("unsupported!");
"x-session-token": session?.token, return;
};
async function login() {
state.auth.setSession(session);
} }
const { onboarding } = await client.req( const s = session;
"GET",
"/onboard/hello", client.session = session;
); (client as any).$updateHeaders();
async function login() {
state.auth.setSession(s);
}
const { onboarding } = await client.api.get("/onboard/hello");
if (onboarding) { if (onboarding) {
openScreen({ openScreen({

View file

@ -16,7 +16,7 @@ export function FormSendReset() {
<Form <Form
page="send_reset" page="send_reset"
callback={async (data) => { callback={async (data) => {
await client.req("POST", "/auth/account/reset_password", data); await client.api.post("/auth/account/reset_password", data);
}} }}
/> />
); );
@ -32,7 +32,7 @@ export function FormReset() {
<Form <Form
page="reset" page="reset"
callback={async (data) => { callback={async (data) => {
await client.req("PATCH", "/auth/account/reset_password", { await client.api.patch("/auth/account/reset_password", {
token, token,
...data, ...data,
}); });

View file

@ -20,7 +20,7 @@ export function FormResend() {
<Form <Form
page="resend" page="resend"
callback={async (data) => { callback={async (data) => {
await client.req("POST", "/auth/account/reverify", data); await client.api.post("/auth/account/reverify", data);
}} }}
/> />
); );
@ -34,11 +34,8 @@ export function FormVerify() {
const history = useHistory(); const history = useHistory();
useEffect(() => { useEffect(() => {
client client.api
.req( .post(`/auth/account/verify/${token as ""}`)
"POST",
`/auth/account/verify/${token}` as "/auth/account/verify/:code",
)
.then(() => history.push("/login")) .then(() => history.push("/login"))
.catch((err) => setError(takeError(err))); .catch((err) => setError(takeError(err)));
// eslint-disable-next-line // eslint-disable-next-line

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