mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-25 08:30:58 -05:00
merge: pull request #717 from revoltchat/chore/client-fsm
This commit is contained in:
commit
5dfe72c093
126 changed files with 1761 additions and 1369 deletions
2
external/lang
vendored
2
external/lang
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 50838167d7d253de9d08715e6a6070c3ddc9fcc2
|
Subproject commit d4bc47b729c7e69ce97216469692b39f4cd1640e
|
|
@ -106,6 +106,7 @@
|
||||||
"eslint": "^7.28.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-config-preact": "^1.1.4",
|
"eslint-config-preact": "^1.1.4",
|
||||||
"eslint-plugin-jsdoc": "^39.3.2",
|
"eslint-plugin-jsdoc": "^39.3.2",
|
||||||
|
"eslint-plugin-mobx": "^0.0.8",
|
||||||
"eventemitter3": "^4.0.7",
|
"eventemitter3": "^4.0.7",
|
||||||
"history": "4",
|
"history": "4",
|
||||||
"json-stringify-deterministic": "^1.0.2",
|
"json-stringify-deterministic": "^1.0.2",
|
||||||
|
|
|
@ -3,9 +3,8 @@ import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
import { StateUpdater, useState } from "preact/hooks";
|
import { StateUpdater, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import { emojiDictionary } from "../../assets/emojis";
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import ChannelIcon from "./ChannelIcon";
|
import ChannelIcon from "./ChannelIcon";
|
||||||
import Emoji from "./Emoji";
|
import Emoji from "./Emoji";
|
||||||
import UserIcon from "./user/UserIcon";
|
import UserIcon from "./user/UserIcon";
|
||||||
|
|
|
@ -2,12 +2,9 @@ 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";
|
import { Channel } from "revolt.js";
|
||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import fallback from "./assets/group.png";
|
import fallback from "./assets/group.png";
|
||||||
|
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
||||||
|
|
||||||
interface Props extends IconBaseProps<Channel> {
|
interface Props extends IconBaseProps<Channel> {
|
||||||
|
@ -22,7 +19,7 @@ export default observer(
|
||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { IconButton } from "@revoltchat/ui";
|
import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { modalController } from "../../controllers/modals/ModalController";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -60,6 +61,9 @@ const ServerBanner = styled.div<Omit<Props, "server">>`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -121,7 +125,13 @@ export default observer(({ server }: Props) => {
|
||||||
</svg>
|
</svg>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<div className="title">{server.name}</div>
|
<a
|
||||||
|
className="title"
|
||||||
|
onClick={() =>
|
||||||
|
modalController.push({ type: "server_info", server })
|
||||||
|
}>
|
||||||
|
{server.name}
|
||||||
|
</a>
|
||||||
{server.havePermission("ManageServer") && (
|
{server.havePermission("ManageServer") && (
|
||||||
<Link to={`/server/${server._id}/settings`}>
|
<Link to={`/server/${server._id}/settings`}>
|
||||||
<IconButton>
|
<IconButton>
|
||||||
|
|
|
@ -4,8 +4,7 @@ import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
|
||||||
import { IconBaseProps, ImageIconBase } from "./IconBase";
|
import { IconBaseProps, ImageIconBase } from "./IconBase";
|
||||||
|
|
||||||
interface Props extends IconBaseProps<Server> {
|
interface Props extends IconBaseProps<Server> {
|
||||||
|
@ -34,7 +33,7 @@ export default observer(
|
||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
|
|
||||||
const { target, attachment, size, animate, server_name, ...imgProps } =
|
const { target, attachment, size, animate, server_name, ...imgProps } =
|
||||||
props;
|
props;
|
||||||
|
|
|
@ -14,8 +14,8 @@ import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import { I18nError } from "../../../context/Locale";
|
import { I18nError } from "../../../context/Locale";
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import Markdown from "../../markdown/Markdown";
|
import Markdown from "../../markdown/Markdown";
|
||||||
import UserIcon from "../user/UserIcon";
|
import UserIcon from "../user/UserIcon";
|
||||||
import { Username } from "../user/UserShort";
|
import { Username } from "../user/UserShort";
|
||||||
|
@ -52,7 +52,7 @@ const Message = observer(
|
||||||
queued,
|
queued,
|
||||||
hideReply,
|
hideReply,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const client = useClient();
|
const client = message.client;
|
||||||
const user = message.author;
|
const user = message.author;
|
||||||
|
|
||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
|
@ -70,7 +70,10 @@ const Message = observer(
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const openProfile = () =>
|
const openProfile = () =>
|
||||||
openScreen({ id: "profile", user_id: message.author_id });
|
modalController.push({
|
||||||
|
type: "user_profile",
|
||||||
|
user_id: message.author_id,
|
||||||
|
});
|
||||||
|
|
||||||
const handleUserClick = (e: MouseEvent) => {
|
const handleUserClick = (e: MouseEvent) => {
|
||||||
if (e.shiftKey && user?._id) {
|
if (e.shiftKey && user?._id) {
|
||||||
|
|
|
@ -24,15 +24,15 @@ import {
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
import { Reply } from "../../../mobx/stores/MessageQueue";
|
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
|
||||||
import {
|
import {
|
||||||
FileUploader,
|
FileUploader,
|
||||||
grabFiles,
|
grabFiles,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
} from "../../../context/revoltjs/FileUploads";
|
} from "../../../context/revoltjs/FileUploads";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../../context/revoltjs/util";
|
import { takeError } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||||
import { PermissionTooltip } from "../Tooltip";
|
import { PermissionTooltip } from "../Tooltip";
|
||||||
import FilePreview from "./bars/FilePreview";
|
import FilePreview from "./bars/FilePreview";
|
||||||
|
@ -148,7 +148,7 @@ export default observer(({ channel }: Props) => {
|
||||||
});
|
});
|
||||||
const [typing, setTyping] = useState<boolean | number>(false);
|
const [typing, setTyping] = useState<boolean | number>(false);
|
||||||
const [replies, setReplies] = useState<Reply[]>([]);
|
const [replies, setReplies] = useState<Reply[]>([]);
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const translate = useTranslation();
|
const translate = useTranslation();
|
||||||
|
|
||||||
const renderer = getRenderer(channel);
|
const renderer = getRenderer(channel);
|
||||||
|
|
|
@ -3,10 +3,9 @@ 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";
|
||||||
import { useTriggerEvents } from "preact-context-menu";
|
import { useTriggerEvents } from "preact-context-menu";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import AttachmentActions from "./AttachmentActions";
|
import AttachmentActions from "./AttachmentActions";
|
||||||
import { SizedGrid } from "./Grid";
|
import { SizedGrid } from "./Grid";
|
||||||
import ImageFile from "./ImageFile";
|
import ImageFile from "./ImageFile";
|
||||||
|
@ -21,7 +20,7 @@ interface Props {
|
||||||
const MAX_ATTACHMENT_WIDTH = 480;
|
const MAX_ATTACHMENT_WIDTH = 480;
|
||||||
|
|
||||||
export default function Attachment({ attachment, hasContent }: Props) {
|
export default function Attachment({ attachment, hasContent }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { filename, metadata } = attachment;
|
const { filename, metadata } = attachment;
|
||||||
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,14 @@ import { IconButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { determineFileSize } from "../../../../lib/fileSize";
|
import { determineFileSize } from "../../../../lib/fileSize";
|
||||||
|
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: API.File;
|
attachment: API.File;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AttachmentActions({ attachment }: Props) {
|
export default function AttachmentActions({ attachment }: Props) {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { filename, metadata, size } = attachment;
|
const { filename, metadata, size } = attachment;
|
||||||
|
|
||||||
const url = client.generateFileURL(attachment);
|
const url = client.generateFileURL(attachment);
|
||||||
|
|
|
@ -2,10 +2,10 @@ 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";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
enum ImageLoadingState {
|
enum ImageLoadingState {
|
||||||
Loading,
|
Loading,
|
||||||
|
@ -19,8 +19,7 @@ type Props = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||||
|
|
||||||
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);
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment)!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -32,7 +31,9 @@ export default function ImageFile({ attachment, ...props }: Props) {
|
||||||
className={classNames(styles.image, {
|
className={classNames(styles.image, {
|
||||||
[styles.loading]: loading !== ImageLoadingState.Loaded,
|
[styles.loading]: loading !== ImageLoadingState.Loaded,
|
||||||
})}
|
})}
|
||||||
onClick={() => openScreen({ id: "image_viewer", attachment })}
|
onClick={() =>
|
||||||
|
modalController.push({ type: "image_viewer", attachment })
|
||||||
|
}
|
||||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
||||||
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
||||||
onError={() => setLoading(ImageLoadingState.Error)}
|
onError={() => setLoading(ImageLoadingState.Error)}
|
||||||
|
|
|
@ -3,15 +3,12 @@ import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Attachment.module.scss";
|
import styles from "./Attachment.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button, Preloader } from "@revoltchat/ui";
|
import { Button, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import {
|
import RequiresOnline from "../../../../controllers/client/jsx/RequiresOnline";
|
||||||
AppContext,
|
|
||||||
StatusContext,
|
|
||||||
} from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachment: API.File;
|
attachment: API.File;
|
||||||
|
@ -23,9 +20,8 @@ export default function TextFile({ attachment }: Props) {
|
||||||
const [gated, setGated] = useState(attachment.size > 100_000);
|
const [gated, setGated] = useState(attachment.size > 100_000);
|
||||||
const [content, setContent] = useState<undefined | string>(undefined);
|
const [content, setContent] = useState<undefined | string>(undefined);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const status = useContext(StatusContext);
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
|
|
||||||
|
const client = useClient();
|
||||||
const url = client.generateFileURL(attachment)!;
|
const url = client.generateFileURL(attachment)!;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -56,7 +52,7 @@ export default function TextFile({ attachment }: Props) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [content, loading, gated, status, attachment._id, attachment.size, url]);
|
}, [content, loading, gated, attachment._id, attachment.size, url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -7,7 +7,6 @@ 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 { Permission } from "revolt.js";
|
|
||||||
import { Message as MessageObject } from "revolt.js";
|
import { Message as MessageObject } from "revolt.js";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
@ -24,8 +23,8 @@ import {
|
||||||
Screen,
|
Screen,
|
||||||
useIntermediate,
|
useIntermediate,
|
||||||
} from "../../../../context/intermediate/Intermediate";
|
} from "../../../../context/intermediate/Intermediate";
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import Tooltip from "../../../common/Tooltip";
|
import Tooltip from "../../../common/Tooltip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -88,7 +87,7 @@ const Divider = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MessageOverlayBar = observer(({ message, queued }: Props) => {
|
export const MessageOverlayBar = observer(({ message, queued }: Props) => {
|
||||||
const client = useClient();
|
const client = message.client;
|
||||||
const { openScreen, writeClipboard } = useIntermediate();
|
const { openScreen, writeClipboard } = useIntermediate();
|
||||||
const isAuthor = message.author_id === client.user!._id;
|
const isAuthor = message.author_id === client.user!._id;
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ import classNames from "classnames";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
||||||
import Markdown from "../../../markdown/Markdown";
|
import Markdown from "../../../markdown/Markdown";
|
||||||
import Attachment from "../attachments/Attachment";
|
import Attachment from "../attachments/Attachment";
|
||||||
|
@ -24,7 +25,7 @@ const MAX_PREVIEW_SIZE = 150;
|
||||||
export default function Embed({ embed }: Props) {
|
export default function Embed({ embed }: Props) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const { openScreen, openLink } = useIntermediate();
|
const { openLink } = useIntermediate();
|
||||||
const maxWidth = Math.min(
|
const maxWidth = Math.min(
|
||||||
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
useContext(MessageAreaWidthContext) - CONTAINER_PADDING,
|
||||||
MAX_EMBED_WIDTH,
|
MAX_EMBED_WIDTH,
|
||||||
|
@ -191,7 +192,9 @@ export default function Embed({ embed }: Props) {
|
||||||
type="text/html"
|
type="text/html"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onClick={() => openScreen({ id: "image_viewer", embed })}
|
onClick={() =>
|
||||||
|
modalController.push({ type: "image_viewer", embed })
|
||||||
|
}
|
||||||
onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)}
|
onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Group } from "@styled-icons/boxicons-solid";
|
import { Group } from "@styled-icons/boxicons-solid";
|
||||||
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 { Message, API } from "revolt.js";
|
import { Message, API } from "revolt.js";
|
||||||
|
@ -12,14 +11,13 @@ import { Button, Category, Preloader } from "@revoltchat/ui";
|
||||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { I18nError } from "../../../../context/Locale";
|
import { I18nError } from "../../../../context/Locale";
|
||||||
import {
|
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
} from "../../../../context/revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../../../context/revoltjs/util";
|
import { takeError } from "../../../../context/revoltjs/util";
|
||||||
|
|
||||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||||
|
import {
|
||||||
|
useClient,
|
||||||
|
useSession,
|
||||||
|
} from "../../../../controllers/client/ClientController";
|
||||||
|
|
||||||
const EmbedInviteBase = styled.div`
|
const EmbedInviteBase = styled.div`
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
@ -78,8 +76,8 @@ type Props = {
|
||||||
|
|
||||||
export function EmbedInvite({ code }: Props) {
|
export function EmbedInvite({ code }: Props) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const client = useContext(AppContext);
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
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);
|
||||||
|
@ -90,7 +88,7 @@ export function EmbedInvite({ code }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
typeof invite === "undefined" &&
|
typeof invite === "undefined" &&
|
||||||
(status === ClientStatus.ONLINE || status === ClientStatus.READY)
|
(session.state === "Online" || session.state === "Ready")
|
||||||
) {
|
) {
|
||||||
client
|
client
|
||||||
.fetchInvite(code)
|
.fetchInvite(code)
|
||||||
|
@ -99,7 +97,7 @@ export function EmbedInvite({ code }: Props) {
|
||||||
)
|
)
|
||||||
.catch((err) => setError(takeError(err)));
|
.catch((err) => setError(takeError(err)));
|
||||||
}
|
}
|
||||||
}, [client, code, invite, status]);
|
}, [client, code, invite, session.state]);
|
||||||
|
|
||||||
if (typeof invite === "undefined") {
|
if (typeof invite === "undefined") {
|
||||||
return error ? (
|
return error ? (
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./Embed.module.scss";
|
import styles from "./Embed.module.scss";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
embed: API.Embed;
|
embed: API.Embed;
|
||||||
|
@ -14,7 +14,6 @@ interface Props {
|
||||||
|
|
||||||
export default function EmbedMedia({ embed, width, height }: Props) {
|
export default function EmbedMedia({ embed, width, height }: Props) {
|
||||||
if (embed.type !== "Website") return null;
|
if (embed.type !== "Website") return null;
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
switch (embed.special?.type) {
|
switch (embed.special?.type) {
|
||||||
|
@ -117,8 +116,8 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "image_viewer",
|
type: "image_viewer",
|
||||||
embed: embed.image!,
|
embed: embed.image!,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,9 @@ import styled, { css } from "styled-components/macro";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import fallback from "../assets/user.png";
|
import fallback from "../assets/user.png";
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import IconBase, { IconBaseProps } from "../IconBase";
|
import IconBase, { IconBaseProps } from "../IconBase";
|
||||||
|
|
||||||
type VoiceStatus = "muted" | "deaf";
|
type VoiceStatus = "muted" | "deaf";
|
||||||
|
@ -56,8 +55,7 @@ export default observer(
|
||||||
keyof Props | "children" | "as"
|
keyof Props | "children" | "as"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
// ! TODO: this is temporary code
|
const client = useClient();
|
||||||
const client = useClient() ?? useApplicationState().client!;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
target,
|
target,
|
||||||
|
|
|
@ -8,9 +8,8 @@ import { Text } from "preact-i18n";
|
||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
import UserIcon from "./UserIcon";
|
import UserIcon from "./UserIcon";
|
||||||
|
|
||||||
const BotBadge = styled.div`
|
const BotBadge = styled.div`
|
||||||
|
@ -125,9 +124,9 @@ export default function UserShort({
|
||||||
masquerade?: API.Masquerade;
|
masquerade?: API.Masquerade;
|
||||||
showServerIdentity?: boolean;
|
showServerIdentity?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
const openProfile = () =>
|
const openProfile = () =>
|
||||||
user && openScreen({ id: "profile", user_id: user._id });
|
user &&
|
||||||
|
modalController.push({ type: "user_profile", user_id: user._id });
|
||||||
|
|
||||||
const handleUserClick = (e: MouseEvent) => {
|
const handleUserClick = (e: MouseEvent) => {
|
||||||
if (e.shiftKey && user?._id) {
|
if (e.shiftKey && user?._id) {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
.markdown {
|
.markdown {
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
:global(.emoji) {
|
:global(.emoji) {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,9 @@ import { determineLink } from "../../lib/links";
|
||||||
|
|
||||||
import { dayjs } from "../../context/Locale";
|
import { dayjs } from "../../context/Locale";
|
||||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import { emojiDictionary } from "../../assets/emojis";
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import { generateEmoji } from "../common/Emoji";
|
import { generateEmoji } from "../common/Emoji";
|
||||||
import { MarkdownProps } from "./Markdown";
|
import { MarkdownProps } from "./Markdown";
|
||||||
import Prism from "./prism";
|
import Prism from "./prism";
|
||||||
|
@ -118,7 +118,7 @@ const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
|
||||||
const RE_TIME = /<t:([0-9]+):(\w)>/g;
|
const RE_TIME = /<t:([0-9]+):(\w)>/g;
|
||||||
|
|
||||||
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const { openLink } = useIntermediate();
|
const { openLink } = useIntermediate();
|
||||||
|
|
||||||
if (typeof content === "undefined") return null;
|
if (typeof content === "undefined") return null;
|
||||||
|
|
|
@ -9,8 +9,7 @@ import ConditionalLink from "../../lib/ConditionalLink";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
|
||||||
import UserIcon from "../common/user/UserIcon";
|
import UserIcon from "../common/user/UserIcon";
|
||||||
|
|
||||||
const Base = styled.div`
|
const Base = styled.div`
|
||||||
|
|
|
@ -1,45 +1,47 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Banner } from "@revoltchat/ui";
|
import { Banner, Button, Column } from "@revoltchat/ui";
|
||||||
|
|
||||||
import {
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
useClient,
|
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
export default function ConnectionStatus() {
|
function ConnectionStatus() {
|
||||||
const status = useContext(StatusContext);
|
const session = useSession()!;
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
if (status === ClientStatus.OFFLINE) {
|
if (session.state === "Offline") {
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
<Text id="app.special.status.offline" />
|
<Text id="app.special.status.offline" />
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
} else if (status === ClientStatus.DISCONNECTED) {
|
} else if (session.state === "Disconnected") {
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
<Text id="app.special.status.disconnected" /> <br />
|
<Column centred>
|
||||||
<a onClick={() => client.websocket.connect()}>
|
<Text id="app.special.status.disconnected" />
|
||||||
<Text id="app.special.status.reconnect" />
|
<Button
|
||||||
</a>
|
compact
|
||||||
|
palette="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
session.emit({
|
||||||
|
action: "RETRY",
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<Text id="app.status.reconnect" />
|
||||||
|
</Button>
|
||||||
|
</Column>
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
} else if (status === ClientStatus.CONNECTING) {
|
} else if (session.state === "Connecting") {
|
||||||
return (
|
|
||||||
<Banner>
|
|
||||||
<Text id="app.special.status.connecting" />
|
|
||||||
</Banner>
|
|
||||||
);
|
|
||||||
} else if (status === ClientStatus.RECONNECTING) {
|
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
<Text id="app.special.status.reconnecting" />
|
<Text id="app.special.status.reconnecting" />
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default observer(ConnectionStatus);
|
||||||
|
|
|
@ -21,10 +21,10 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import placeholderSVG from "../items/placeholder.svg";
|
import placeholderSVG from "../items/placeholder.svg";
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||||
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
|
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
|
||||||
import ConnectionStatus from "../items/ConnectionStatus";
|
import ConnectionStatus from "../items/ConnectionStatus";
|
||||||
|
@ -46,7 +46,7 @@ const Navbar = styled.div`
|
||||||
|
|
||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const state = useApplicationState();
|
const state = useApplicationState();
|
||||||
const { channel: channel_id } = useParams<{ channel: string }>();
|
const { channel: channel_id } = useParams<{ channel: string }>();
|
||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
|
|
|
@ -8,7 +8,8 @@ import { ServerList } from "@revoltchat/ui";
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server list sidebar shim component
|
* Server list sidebar shim component
|
||||||
|
|
|
@ -14,8 +14,7 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { useApplicationState } from "../../../mobx/State";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
|
||||||
import CollapsibleSection from "../../common/CollapsibleSection";
|
import CollapsibleSection from "../../common/CollapsibleSection";
|
||||||
import ServerHeader from "../../common/ServerHeader";
|
import ServerHeader from "../../common/ServerHeader";
|
||||||
import { ChannelButton } from "../items/ButtonItem";
|
import { ChannelButton } from "../items/ButtonItem";
|
||||||
|
|
|
@ -8,11 +8,7 @@ import { memo } from "preact/compat";
|
||||||
|
|
||||||
import { internalEmit } from "../../../lib/eventEmitter";
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
|
|
||||||
import {
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
Screen,
|
|
||||||
useIntermediate,
|
|
||||||
} from "../../../context/intermediate/Intermediate";
|
|
||||||
|
|
||||||
import { UserButton } from "../items/ButtonItem";
|
import { UserButton } from "../items/ButtonItem";
|
||||||
|
|
||||||
export type MemberListGroup = {
|
export type MemberListGroup = {
|
||||||
|
@ -55,15 +51,7 @@ const NoOomfie = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ItemContent = memo(
|
const ItemContent = memo(
|
||||||
({
|
({ item, context }: { item: User; context: Channel }) => (
|
||||||
item,
|
|
||||||
context,
|
|
||||||
openScreen,
|
|
||||||
}: {
|
|
||||||
item: User;
|
|
||||||
context: Channel;
|
|
||||||
openScreen: (screen: Screen) => void;
|
|
||||||
}) => (
|
|
||||||
<UserButton
|
<UserButton
|
||||||
key={item._id}
|
key={item._id}
|
||||||
user={item}
|
user={item}
|
||||||
|
@ -77,13 +65,12 @@ const ItemContent = memo(
|
||||||
`<@${item._id}>`,
|
`<@${item._id}>`,
|
||||||
"mention",
|
"mention",
|
||||||
);
|
);
|
||||||
} else
|
} else {
|
||||||
[
|
modalController.push({
|
||||||
openScreen({
|
type: "user_profile",
|
||||||
id: "profile",
|
|
||||||
user_id: item._id,
|
user_id: item._id,
|
||||||
}),
|
});
|
||||||
];
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -96,8 +83,6 @@ export default function MemberList({
|
||||||
entries: MemberListGroup[];
|
entries: MemberListGroup[];
|
||||||
context: Channel;
|
context: Channel;
|
||||||
}) {
|
}) {
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupedVirtuoso
|
<GroupedVirtuoso
|
||||||
groupCounts={entries.map((x) => x.users.length)}
|
groupCounts={entries.map((x) => x.users.length)}
|
||||||
|
@ -137,7 +122,8 @@ export default function MemberList({
|
||||||
server, see issue{" "}
|
server, see issue{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/revoltchat/delta/issues/128"
|
href="https://github.com/revoltchat/delta/issues/128"
|
||||||
target="_blank" rel="noreferrer">
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
#128
|
#128
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
for when this will be resolved.
|
for when this will be resolved.
|
||||||
|
@ -158,11 +144,7 @@ export default function MemberList({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ItemContent
|
<ItemContent item={item} context={context} />
|
||||||
item={item}
|
|
||||||
context={context}
|
|
||||||
openScreen={openScreen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -4,14 +4,12 @@ import { observer } from "mobx-react-lite";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Channel, Server, User, API } from "revolt.js";
|
import { Channel, Server, User, API } from "revolt.js";
|
||||||
|
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClientStatus,
|
useSession,
|
||||||
StatusContext,
|
|
||||||
useClient,
|
useClient,
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
} from "../../../controllers/client/ClientController";
|
||||||
|
|
||||||
import { GenericSidebarBase } from "../SidebarBase";
|
import { GenericSidebarBase } from "../SidebarBase";
|
||||||
import MemberList, { MemberListGroup } from "./MemberList";
|
import MemberList, { MemberListGroup } from "./MemberList";
|
||||||
|
|
||||||
|
@ -205,18 +203,18 @@ function shouldSkipOffline(id: string) {
|
||||||
|
|
||||||
export const ServerMemberSidebar = observer(
|
export const ServerMemberSidebar = observer(
|
||||||
({ channel }: { channel: Channel }) => {
|
({ channel }: { channel: Channel }) => {
|
||||||
const client = useClient();
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const server_id = channel.server_id!;
|
const server_id = channel.server_id!;
|
||||||
if (status === ClientStatus.ONLINE && !FETCHED.has(server_id)) {
|
if (session.state === "Online" && !FETCHED.has(server_id)) {
|
||||||
FETCHED.add(server_id);
|
FETCHED.add(server_id);
|
||||||
channel
|
channel
|
||||||
.server!.syncMembers(shouldSkipOffline(server_id))
|
.server!.syncMembers(shouldSkipOffline(server_id))
|
||||||
.catch(() => FETCHED.delete(server_id));
|
.catch(() => FETCHED.delete(server_id));
|
||||||
}
|
}
|
||||||
}, [status, channel]);
|
}, [session.state, channel]);
|
||||||
|
|
||||||
const entries = useEntries(
|
const entries = useEntries(
|
||||||
channel,
|
channel,
|
||||||
|
|
|
@ -7,8 +7,7 @@ import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
|
||||||
import Message from "../../common/messaging/Message";
|
import Message from "../../common/messaging/Message";
|
||||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,16 @@ import { Block } from "@styled-icons/boxicons-regular";
|
||||||
import { Trash } from "@styled-icons/boxicons-solid";
|
import { Trash } from "@styled-icons/boxicons-solid";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { CategoryButton } from "@revoltchat/ui";
|
import { CategoryButton } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
|
||||||
import {
|
import {
|
||||||
LogOutContext,
|
clientController,
|
||||||
useClient,
|
useClient,
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
} from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
export default function AccountManagement() {
|
export default function AccountManagement() {
|
||||||
const logOut = useContext(LogOutContext);
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const callback = (route: "disable" | "delete") => () =>
|
const callback = (route: "disable" | "delete") => () =>
|
||||||
|
@ -26,7 +24,7 @@ export default function AccountManagement() {
|
||||||
"X-MFA-Ticket": ticket.token,
|
"X-MFA-Ticket": ticket.token,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => logOut(true)),
|
.then(clientController.logoutCurrent),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Envelope, Key, Pencil } from "@styled-icons/boxicons-solid";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AccountDetail,
|
AccountDetail,
|
||||||
|
@ -12,26 +12,22 @@ import {
|
||||||
HiddenValue,
|
HiddenValue,
|
||||||
} from "@revoltchat/ui";
|
} from "@revoltchat/ui";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
import {
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
useClient,
|
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
const client = useClient();
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
|
|
||||||
const [email, setEmail] = useState("...");
|
const [email, setEmail] = useState("...");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (email === "..." && status === ClientStatus.ONLINE) {
|
if (email === "..." && session.state === "Online") {
|
||||||
client.api
|
client.api
|
||||||
.get("/auth/account/")
|
.get("/auth/account/")
|
||||||
.then((account) => setEmail(account.email));
|
.then((account) => setEmail(account.email));
|
||||||
}
|
}
|
||||||
}, [client, email, status]);
|
}, [client, email, session.state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -3,18 +3,15 @@ import { Lock } from "@styled-icons/boxicons-solid";
|
||||||
import { API } from "revolt.js";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
|
import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { modalController } from "../../../context/modals";
|
|
||||||
import {
|
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
useClient,
|
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../../context/revoltjs/util";
|
import { takeError } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary helper function for Axios config
|
* Temporary helper function for Axios config
|
||||||
* @param token Token
|
* @param token Token
|
||||||
|
@ -33,8 +30,8 @@ export function toConfig(token: string) {
|
||||||
*/
|
*/
|
||||||
export default function MultiFactorAuthentication() {
|
export default function MultiFactorAuthentication() {
|
||||||
// Pull in prerequisites
|
// Pull in prerequisites
|
||||||
const client = useClient();
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
|
|
||||||
// Keep track of MFA state
|
// Keep track of MFA state
|
||||||
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
|
const [mfa, setMFA] = useState<API.MultiFactorStatus>();
|
||||||
|
@ -42,13 +39,13 @@ export default function MultiFactorAuthentication() {
|
||||||
|
|
||||||
// Fetch the current MFA status on account
|
// Fetch the current MFA status on account
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mfa && status === ClientStatus.ONLINE) {
|
if (!mfa && session.state === "Online") {
|
||||||
client.api
|
client!.api
|
||||||
.get("/auth/mfa/")
|
.get("/auth/mfa/")
|
||||||
.then(setMFA)
|
.then(setMFA)
|
||||||
.catch((err) => setError(takeError(err)));
|
.catch((err) => setError(takeError(err)));
|
||||||
}
|
}
|
||||||
}, [client, mfa, status]);
|
}, [mfa, client, session.state]);
|
||||||
|
|
||||||
// Action called when recovery code button is pressed
|
// Action called when recovery code button is pressed
|
||||||
const recoveryAction = useCallback(async () => {
|
const recoveryAction = useCallback(async () => {
|
||||||
|
|
2
src/context/DO_NOT_TOUCH.md
Normal file
2
src/context/DO_NOT_TOUCH.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
hello do not touch `intermediate` or `revoltjs` folders
|
||||||
|
they are being rewritten
|
|
@ -3,3 +3,14 @@ import { createBrowserHistory } from "history";
|
||||||
export const history = createBrowserHistory({
|
export const history = createBrowserHistory({
|
||||||
basename: import.meta.env.BASE_URL,
|
basename: import.meta.env.BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const routeInformation = {
|
||||||
|
getServer: () =>
|
||||||
|
/server\/([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})/.exec(
|
||||||
|
history.location.pathname,
|
||||||
|
)?.[1],
|
||||||
|
getChannel: () =>
|
||||||
|
/channel\/([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})/.exec(
|
||||||
|
history.location.pathname,
|
||||||
|
)?.[1],
|
||||||
|
};
|
||||||
|
|
|
@ -6,21 +6,20 @@ import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Preloader, UIProvider } from "@revoltchat/ui";
|
import { Preloader, UIProvider } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { hydrateState } from "../mobx/State";
|
import { state } from "../mobx/State";
|
||||||
|
|
||||||
|
import Binder from "../controllers/client/jsx/Binder";
|
||||||
|
import ModalRenderer from "../controllers/modals/ModalRenderer";
|
||||||
import Locale from "./Locale";
|
import Locale from "./Locale";
|
||||||
import Theme from "./Theme";
|
import Theme from "./Theme";
|
||||||
import { history } from "./history";
|
import { history } from "./history";
|
||||||
import Intermediate from "./intermediate/Intermediate";
|
import Intermediate from "./intermediate/Intermediate";
|
||||||
import ModalRenderer from "./modals/ModalRenderer";
|
|
||||||
import Client from "./revoltjs/RevoltClient";
|
|
||||||
import SyncManager from "./revoltjs/SyncManager";
|
|
||||||
|
|
||||||
const uiContext = {
|
const uiContext = {
|
||||||
Link,
|
Link,
|
||||||
Text: Text as any,
|
Text: Text as any,
|
||||||
Trigger: ContextMenuTrigger,
|
Trigger: ContextMenuTrigger,
|
||||||
emitAction: () => {},
|
emitAction: () => void {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +30,7 @@ export default function Context({ children }: { children: Children }) {
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hydrateState().then(() => setReady(true));
|
state.hydrate().then(() => setReady(true));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!ready) return <Preloader type="spinner" />;
|
if (!ready) return <Preloader type="spinner" />;
|
||||||
|
@ -41,10 +40,8 @@ export default function Context({ children }: { children: Children }) {
|
||||||
<UIProvider value={uiContext}>
|
<UIProvider value={uiContext}>
|
||||||
<Locale>
|
<Locale>
|
||||||
<Intermediate>
|
<Intermediate>
|
||||||
<Client>
|
|
||||||
{children}
|
{children}
|
||||||
<SyncManager />
|
<Binder />
|
||||||
</Client>
|
|
||||||
</Intermediate>
|
</Intermediate>
|
||||||
<ModalRenderer />
|
<ModalRenderer />
|
||||||
</Locale>
|
</Locale>
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { determineLink } from "../../lib/links";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
import { modalController } from "../modals";
|
import { modalController } from "../../controllers/modals/ModalController";
|
||||||
import Modals from "./Modals";
|
import Modals from "./Modals";
|
||||||
|
|
||||||
export type Screen =
|
export type Screen =
|
||||||
|
@ -159,7 +159,7 @@ export default function Intermediate(props: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const openProfile = (user_id: string) =>
|
const openProfile = (user_id: string) =>
|
||||||
openScreen({ id: "profile", user_id });
|
modalController.push({ type: "user_profile", user_id });
|
||||||
const navigate = (path: string) => history.push(path);
|
const navigate = (path: string) => history.push(path);
|
||||||
|
|
||||||
const subs = [
|
const subs = [
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
//import { isModalClosing } from "../../components/ui/Modal";
|
//import { isModalClosing } from "../../components/ui/Modal";
|
||||||
import { Screen } from "./Intermediate";
|
import { Screen } from "./Intermediate";
|
||||||
import { InputModal } from "./modals/Input";
|
import { InputModal } from "./modals/Input";
|
||||||
import { OnboardingModal } from "./modals/Onboarding";
|
|
||||||
import { PromptModal } from "./modals/Prompt";
|
import { PromptModal } from "./modals/Prompt";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -20,8 +19,6 @@ export default function Modals({ screen, openScreen }: Props) {
|
||||||
return <PromptModal onClose={onClose} {...screen} />;
|
return <PromptModal onClose={onClose} {...screen} />;
|
||||||
case "_input":
|
case "_input":
|
||||||
return <InputModal onClose={onClose} {...screen} />;
|
return <InputModal onClose={onClose} {...screen} />;
|
||||||
case "onboarding":
|
|
||||||
return <OnboardingModal onClose={onClose} {...screen} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -3,11 +3,6 @@ import { useContext } from "preact/hooks";
|
||||||
import { IntermediateContext, useIntermediate } from "./Intermediate";
|
import { IntermediateContext, useIntermediate } from "./Intermediate";
|
||||||
import { SpecialInputModal } from "./modals/Input";
|
import { SpecialInputModal } from "./modals/Input";
|
||||||
import { SpecialPromptModal } from "./modals/Prompt";
|
import { SpecialPromptModal } from "./modals/Prompt";
|
||||||
import { ChannelInfo } from "./popovers/ChannelInfo";
|
|
||||||
import { CreateBotModal } from "./popovers/CreateBot";
|
|
||||||
import { ImageViewer } from "./popovers/ImageViewer";
|
|
||||||
import { UserPicker } from "./popovers/UserPicker";
|
|
||||||
import { UserProfile } from "./popovers/UserProfile";
|
|
||||||
|
|
||||||
export default function Popovers() {
|
export default function Popovers() {
|
||||||
const { screen } = useContext(IntermediateContext);
|
const { screen } = useContext(IntermediateContext);
|
||||||
|
@ -19,20 +14,6 @@ export default function Popovers() {
|
||||||
//: internalEmit("Modal", "close");
|
//: internalEmit("Modal", "close");
|
||||||
|
|
||||||
switch (screen.id) {
|
switch (screen.id) {
|
||||||
case "profile":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <UserProfile {...screen} onClose={onClose} />;
|
|
||||||
case "user_picker":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <UserPicker {...screen} onClose={onClose} />;
|
|
||||||
case "image_viewer":
|
|
||||||
return <ImageViewer {...screen} onClose={onClose} />;
|
|
||||||
case "channel_info":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <ChannelInfo {...screen} onClose={onClose} />;
|
|
||||||
case "create_bot":
|
|
||||||
// @ts-expect-error someone figure this out :)
|
|
||||||
return <CreateBotModal onClose={onClose} {...screen} />;
|
|
||||||
case "special_prompt":
|
case "special_prompt":
|
||||||
// @ts-expect-error someone figure this out :)
|
// @ts-expect-error someone figure this out :)
|
||||||
return <SpecialPromptModal onClose={onClose} {...screen} />;
|
return <SpecialPromptModal onClose={onClose} {...screen} />;
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { useContext, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Category, InputBox, Modal } from "@revoltchat/ui";
|
import { Category, InputBox, Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import { I18nError } from "../../Locale";
|
import { I18nError } from "../../Locale";
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { takeError } from "../../revoltjs/util";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -89,7 +89,7 @@ type SpecialProps = { onClose: () => void } & (
|
||||||
|
|
||||||
export function SpecialInputModal(props: SpecialProps) {
|
export function SpecialInputModal(props: SpecialProps) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
|
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ulid } from "ulid";
|
||||||
|
|
||||||
import styles from "./Prompt.module.scss";
|
import styles from "./Prompt.module.scss";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Category, Modal, InputBox, Radio } from "@revoltchat/ui";
|
import { Category, Modal, InputBox, Radio } from "@revoltchat/ui";
|
||||||
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
||||||
|
@ -14,8 +14,8 @@ import { TextReact } from "../../../lib/i18n";
|
||||||
|
|
||||||
import Message from "../../../components/common/messaging/Message";
|
import Message from "../../../components/common/messaging/Message";
|
||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
import UserIcon from "../../../components/common/user/UserIcon";
|
||||||
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
import { I18nError } from "../../Locale";
|
import { I18nError } from "../../Locale";
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { takeError } from "../../revoltjs/util";
|
||||||
import { useIntermediate } from "../Intermediate";
|
import { useIntermediate } from "../Intermediate";
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ type SpecialProps = { onClose: () => void } & (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SpecialPromptModal = observer((props: SpecialProps) => {
|
export const SpecialPromptModal = observer((props: SpecialProps) => {
|
||||||
const client = useContext(AppContext);
|
const client = useClient();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [error, setError] = useState<undefined | string>(undefined);
|
const [error, setError] = useState<undefined | string>(undefined);
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
.info {
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { X } from "@styled-icons/boxicons-regular";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Channel } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./ChannelInfo.module.scss";
|
|
||||||
|
|
||||||
import { Modal } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
|
||||||
import { getChannelName } from "../../revoltjs/util";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
channel: Channel;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChannelInfo = observer(({ channel, onClose }: Props) => {
|
|
||||||
if (
|
|
||||||
channel.channel_type === "DirectMessage" ||
|
|
||||||
channel.channel_type === "SavedMessages"
|
|
||||||
) {
|
|
||||||
onClose();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal onClose={onClose}>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<h1>{getChannelName(channel, true)}</h1>
|
|
||||||
<div onClick={onClose}>
|
|
||||||
<X size={36} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<Markdown content={channel.description!} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -1,20 +0,0 @@
|
||||||
.viewer {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
border-end-end-radius: 4px;
|
|
||||||
border-end-start-radius: 4px;
|
|
||||||
|
|
||||||
max-width: 100vw;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 75vh;
|
|
||||||
object-fit: contain;
|
|
||||||
border-bottom: thin solid var(--tertiary-foreground);
|
|
||||||
|
|
||||||
-webkit-touch-callout: default;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useClient } from "./RevoltClient";
|
import { clientController } from "../../controllers/client/ClientController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
|
@ -11,18 +12,30 @@ interface Props {
|
||||||
children: Children;
|
children: Children;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CheckAuth = (props: Props) => {
|
/**
|
||||||
const auth = useApplicationState().auth;
|
* Check that we are logged in or out and redirect accordingly.
|
||||||
const client = useClient();
|
* Also prevent render until the client is ready to display.
|
||||||
const ready = auth.isLoggedIn() && !!client?.user;
|
*/
|
||||||
|
export const CheckAuth = observer((props: Props) => {
|
||||||
|
const loggedIn = clientController.isLoggedIn();
|
||||||
|
|
||||||
if (props.auth && !ready) {
|
// Redirect if logged out on authenticated page or vice-versa.
|
||||||
|
if (props.auth && !loggedIn) {
|
||||||
if (props.blockRender) return null;
|
if (props.blockRender) return null;
|
||||||
return <Redirect to="/login" />;
|
return <Redirect to="/login" />;
|
||||||
} else if (!props.auth && ready) {
|
} else if (!props.auth && loggedIn) {
|
||||||
if (props.blockRender) return null;
|
if (props.blockRender) return null;
|
||||||
return <Redirect to="/" />;
|
return <Redirect to="/" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block render if client is getting ready to work.
|
||||||
|
if (
|
||||||
|
props.auth &&
|
||||||
|
clientController.isLoggedIn() &&
|
||||||
|
!clientController.isReady()
|
||||||
|
) {
|
||||||
|
return <Preloader type="spinner" />;
|
||||||
|
}
|
||||||
|
|
||||||
return <>{props.children}</>;
|
return <>{props.children}</>;
|
||||||
};
|
});
|
||||||
|
|
|
@ -5,17 +5,15 @@ import Axios, { AxiosRequestConfig } from "axios";
|
||||||
import styles from "./FileUploads.module.scss";
|
import styles from "./FileUploads.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { IconButton, Preloader } from "@revoltchat/ui";
|
import { IconButton, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { determineFileSize } from "../../lib/fileSize";
|
import { determineFileSize } from "../../lib/fileSize";
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../controllers/modals/ModalController";
|
||||||
import { useIntermediate } from "../intermediate/Intermediate";
|
import { useIntermediate } from "../intermediate/Intermediate";
|
||||||
import { modalController } from "../modals";
|
|
||||||
import { AppContext } from "./RevoltClient";
|
|
||||||
import { takeError } from "./util";
|
import { takeError } from "./util";
|
||||||
|
|
||||||
type BehaviourType =
|
type BehaviourType =
|
||||||
|
@ -115,7 +113,7 @@ export function grabFiles(
|
||||||
export function FileUploader(props: Props) {
|
export function FileUploader(props: Props) {
|
||||||
const { fileType, maxFileSize, remove } = props;
|
const { fileType, maxFileSize, remove } = props;
|
||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
const client = useApplicationState().client!;
|
const client = useClient();
|
||||||
|
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
|
|
@ -1,296 +0,0 @@
|
||||||
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
|
||||||
import { Message, User } from "revolt.js";
|
|
||||||
import { decodeTime } from "ulid";
|
|
||||||
|
|
||||||
import { useCallback, useContext, useEffect } from "preact/hooks";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../lib/i18n";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
|
||||||
|
|
||||||
import { AppContext } from "./RevoltClient";
|
|
||||||
|
|
||||||
const notifications: { [key: string]: Notification } = {};
|
|
||||||
|
|
||||||
async function createNotification(
|
|
||||||
title: string,
|
|
||||||
options: globalThis.NotificationOptions,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
return new Notification(title, options);
|
|
||||||
} catch (err) {
|
|
||||||
const sw = await navigator.serviceWorker.getRegistration();
|
|
||||||
sw?.showNotification(title, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Notifier() {
|
|
||||||
const translate = useTranslation();
|
|
||||||
const state = useApplicationState();
|
|
||||||
const notifs = state.notifications;
|
|
||||||
const showNotification = state.settings.get("notifications:desktop");
|
|
||||||
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
const { guild: guild_id, channel: channel_id } = useParams<{
|
|
||||||
guild: string;
|
|
||||||
channel: string;
|
|
||||||
}>();
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const message = useCallback(
|
|
||||||
async (msg: Message) => {
|
|
||||||
if (msg.channel_id === channel_id && document.hasFocus()) return;
|
|
||||||
if (!notifs.shouldNotify(msg)) return;
|
|
||||||
|
|
||||||
state.settings.sounds.playSound("message");
|
|
||||||
if (!showNotification) return;
|
|
||||||
|
|
||||||
const effectiveName = msg.masquerade?.name ?? msg.author?.username;
|
|
||||||
|
|
||||||
let title;
|
|
||||||
switch (msg.channel?.channel_type) {
|
|
||||||
case "SavedMessages":
|
|
||||||
return;
|
|
||||||
case "DirectMessage":
|
|
||||||
title = `@${effectiveName}`;
|
|
||||||
break;
|
|
||||||
case "Group":
|
|
||||||
if (msg.author?._id === "00000000000000000000000000") {
|
|
||||||
title = msg.channel.name;
|
|
||||||
} else {
|
|
||||||
title = `@${effectiveName} - ${msg.channel.name}`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "TextChannel":
|
|
||||||
title = `@${effectiveName} (#${msg.channel.name}, ${msg.channel.server?.name})`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
title = msg.channel?._id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image;
|
|
||||||
if (msg.attachments) {
|
|
||||||
const imageAttachment = msg.attachments.find(
|
|
||||||
(x) => x.metadata.type === "Image",
|
|
||||||
);
|
|
||||||
if (imageAttachment) {
|
|
||||||
image = client.generateFileURL(imageAttachment, {
|
|
||||||
max_side: 720,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let body, icon;
|
|
||||||
if (msg.content) {
|
|
||||||
body = client.markdownToText(msg.content);
|
|
||||||
|
|
||||||
if (msg.masquerade?.avatar) {
|
|
||||||
icon = client.proxyFile(msg.masquerade.avatar);
|
|
||||||
} else {
|
|
||||||
icon = msg.author?.generateAvatarURL({ max_side: 256 });
|
|
||||||
}
|
|
||||||
} else if (msg.system) {
|
|
||||||
const users = client.users;
|
|
||||||
|
|
||||||
switch (msg.system.type) {
|
|
||||||
case "user_added":
|
|
||||||
case "user_remove":
|
|
||||||
{
|
|
||||||
const user = users.get(msg.system.id);
|
|
||||||
body = translate(
|
|
||||||
`app.main.channel.system.${
|
|
||||||
msg.system.type === "user_added"
|
|
||||||
? "added_by"
|
|
||||||
: "removed_by"
|
|
||||||
}`,
|
|
||||||
{
|
|
||||||
user: user?.username,
|
|
||||||
other_user: users.get(msg.system.by)
|
|
||||||
?.username,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "user_joined":
|
|
||||||
case "user_left":
|
|
||||||
case "user_kicked":
|
|
||||||
case "user_banned":
|
|
||||||
{
|
|
||||||
const user = users.get(msg.system.id);
|
|
||||||
body = translate(
|
|
||||||
`app.main.channel.system.${msg.system.type}`,
|
|
||||||
{ user: user?.username },
|
|
||||||
);
|
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "channel_renamed":
|
|
||||||
{
|
|
||||||
const user = users.get(msg.system.by);
|
|
||||||
body = translate(
|
|
||||||
`app.main.channel.system.channel_renamed`,
|
|
||||||
{
|
|
||||||
user: users.get(msg.system.by)?.username,
|
|
||||||
name: msg.system.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "channel_description_changed":
|
|
||||||
case "channel_icon_changed":
|
|
||||||
{
|
|
||||||
const user = users.get(msg.system.by);
|
|
||||||
body = translate(
|
|
||||||
`app.main.channel.system.${msg.system.type}`,
|
|
||||||
{ user: users.get(msg.system.by)?.username },
|
|
||||||
);
|
|
||||||
icon = user?.generateAvatarURL({
|
|
||||||
max_side: 256,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const notif = await createNotification(title!, {
|
|
||||||
icon,
|
|
||||||
image,
|
|
||||||
body,
|
|
||||||
timestamp: decodeTime(msg._id),
|
|
||||||
tag: msg.channel?._id,
|
|
||||||
badge: "/assets/icons/android-chrome-512x512.png",
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (notif) {
|
|
||||||
notif.addEventListener("click", () => {
|
|
||||||
window.focus();
|
|
||||||
const id = msg.channel_id;
|
|
||||||
if (id !== channel_id) {
|
|
||||||
const channel = client.channels.get(id);
|
|
||||||
if (channel) {
|
|
||||||
if (channel.channel_type === "TextChannel") {
|
|
||||||
history.push(
|
|
||||||
`/server/${channel.server_id}/channel/${id}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
history.push(`/channel/${id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
notifications[msg.channel_id] = notif;
|
|
||||||
notif.addEventListener(
|
|
||||||
"close",
|
|
||||||
() => delete notifications[msg.channel_id],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
history,
|
|
||||||
showNotification,
|
|
||||||
translate,
|
|
||||||
channel_id,
|
|
||||||
client,
|
|
||||||
notifs,
|
|
||||||
state,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const relationship = useCallback(
|
|
||||||
async (user: User) => {
|
|
||||||
if (client.user?.status?.presence === "Busy") return;
|
|
||||||
if (!showNotification) return;
|
|
||||||
|
|
||||||
let event;
|
|
||||||
switch (user.relationship) {
|
|
||||||
case "Incoming":
|
|
||||||
event = translate("notifications.sent_request", {
|
|
||||||
person: user.username,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "Friend":
|
|
||||||
event = translate("notifications.now_friends", {
|
|
||||||
person: user.username,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const notif = await createNotification(event, {
|
|
||||||
icon: user.generateAvatarURL({ max_side: 256 }),
|
|
||||||
badge: "/assets/icons/android-chrome-512x512.png",
|
|
||||||
timestamp: +new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
notif?.addEventListener("click", () => {
|
|
||||||
history.push(`/friends`);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[client.user?.status?.presence, history, showNotification, translate],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client.addListener("message", message);
|
|
||||||
client.addListener("user/relationship", relationship);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
client.removeListener("message", message);
|
|
||||||
client.removeListener("user/relationship", relationship);
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
client,
|
|
||||||
state,
|
|
||||||
guild_id,
|
|
||||||
channel_id,
|
|
||||||
showNotification,
|
|
||||||
notifs,
|
|
||||||
message,
|
|
||||||
relationship,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function visChange() {
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
if (notifications[channel_id]) {
|
|
||||||
notifications[channel_id].close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visChange();
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", visChange);
|
|
||||||
return () =>
|
|
||||||
document.removeEventListener("visibilitychange", visChange);
|
|
||||||
}, [guild_id, channel_id]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NotificationsComponent() {
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path="/server/:server/channel/:channel">
|
|
||||||
<Notifier />
|
|
||||||
</Route>
|
|
||||||
<Route path="/channel/:channel">
|
|
||||||
<Notifier />
|
|
||||||
</Route>
|
|
||||||
<Route path="/">
|
|
||||||
<Notifier />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Client } from "revolt.js";
|
|
||||||
|
|
||||||
import { createContext } from "preact";
|
|
||||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Preloader } from "@revoltchat/ui";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
|
||||||
|
|
||||||
import { modalController } from "../modals";
|
|
||||||
import { registerEvents } from "./events";
|
|
||||||
import { takeError } from "./util";
|
|
||||||
|
|
||||||
export enum ClientStatus {
|
|
||||||
READY,
|
|
||||||
LOADING,
|
|
||||||
OFFLINE,
|
|
||||||
DISCONNECTED,
|
|
||||||
CONNECTING,
|
|
||||||
RECONNECTING,
|
|
||||||
ONLINE,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientOperations {
|
|
||||||
logout: (shouldRequest?: boolean) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppContext = createContext<Client>(null!);
|
|
||||||
export const StatusContext = createContext<ClientStatus>(null!);
|
|
||||||
export const LogOutContext = createContext<(avoidReq?: boolean) => void>(null!);
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: Children;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default observer(({ children }: Props) => {
|
|
||||||
const state = useApplicationState();
|
|
||||||
const [client, setClient] = useState<Client>(null!);
|
|
||||||
const [status, setStatus] = useState(ClientStatus.LOADING);
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
|
|
||||||
const logout = useCallback(
|
|
||||||
(avoidReq?: boolean) => {
|
|
||||||
setLoaded(false);
|
|
||||||
client.logout(avoidReq);
|
|
||||||
},
|
|
||||||
[client],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (navigator.onLine) {
|
|
||||||
state.config.createClient().api.get("/").then(state.config.set);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (state.auth.isLoggedIn()) {
|
|
||||||
setLoaded(false);
|
|
||||||
const client = state.config.createClient();
|
|
||||||
setClient(client);
|
|
||||||
|
|
||||||
client
|
|
||||||
.useExistingSession(state.auth.getSession()!)
|
|
||||||
.catch((err) => {
|
|
||||||
const error = takeError(err);
|
|
||||||
if (error === "Forbidden" || error === "Unauthorized") {
|
|
||||||
client.logout(true);
|
|
||||||
modalController.push({ type: "signed_out" });
|
|
||||||
} else {
|
|
||||||
setStatus(ClientStatus.DISCONNECTED);
|
|
||||||
modalController.push({
|
|
||||||
type: "error",
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => setLoaded(true));
|
|
||||||
} else {
|
|
||||||
setStatus(ClientStatus.READY);
|
|
||||||
setLoaded(true);
|
|
||||||
}
|
|
||||||
}, [state.auth.getSession()]);
|
|
||||||
|
|
||||||
useEffect(() => registerEvents(state, setStatus, client), [client]);
|
|
||||||
useEffect(() => state.registerListeners(client), [client]);
|
|
||||||
|
|
||||||
if (!loaded || status === ClientStatus.LOADING) {
|
|
||||||
return <Preloader type="spinner" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppContext.Provider value={client}>
|
|
||||||
<StatusContext.Provider value={status}>
|
|
||||||
<LogOutContext.Provider value={logout}>
|
|
||||||
{children}
|
|
||||||
</LogOutContext.Provider>
|
|
||||||
</StatusContext.Provider>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useClient = () => useContext(AppContext);
|
|
|
@ -1,39 +0,0 @@
|
||||||
/**
|
|
||||||
* This file monitors the message cache to delete any queued messages that have already sent.
|
|
||||||
*/
|
|
||||||
import { Message } from "revolt.js";
|
|
||||||
|
|
||||||
import { useContext, useEffect } from "preact/hooks";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
|
||||||
|
|
||||||
import { setGlobalEmojiPack } from "../../components/common/Emoji";
|
|
||||||
|
|
||||||
import { AppContext } from "./RevoltClient";
|
|
||||||
|
|
||||||
export default function StateMonitor() {
|
|
||||||
const client = useContext(AppContext);
|
|
||||||
const state = useApplicationState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function add(msg: Message) {
|
|
||||||
if (!msg.nonce) return;
|
|
||||||
if (
|
|
||||||
!state.queue.get(msg.channel_id).find((x) => x.id === msg.nonce)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
state.queue.remove(msg.nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.addListener("message", add);
|
|
||||||
return () => client.removeListener("message", add);
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
// Set global emoji pack.
|
|
||||||
useEffect(() => {
|
|
||||||
const v = state.settings.get("appearance:emoji");
|
|
||||||
v && setGlobalEmojiPack(v);
|
|
||||||
}, [state.settings.get("appearance:emoji")]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
/**
|
|
||||||
* This file monitors changes to settings and syncs them to the server.
|
|
||||||
*/
|
|
||||||
import { ClientboundNotification } from "revolt.js";
|
|
||||||
|
|
||||||
import { useEffect } from "preact/hooks";
|
|
||||||
|
|
||||||
import { reportError } from "../../lib/ErrorBoundary";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../../mobx/State";
|
|
||||||
|
|
||||||
import { useClient } from "./RevoltClient";
|
|
||||||
|
|
||||||
export default function SyncManager() {
|
|
||||||
const client = useClient();
|
|
||||||
const state = useApplicationState();
|
|
||||||
|
|
||||||
// Sync settings from Revolt.
|
|
||||||
useEffect(() => {
|
|
||||||
if (client) {
|
|
||||||
state.sync
|
|
||||||
.pull(client)
|
|
||||||
.catch(console.error)
|
|
||||||
.finally(() => state.changelog.checkForUpdates());
|
|
||||||
}
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
// Take data updates from Revolt.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!client) return;
|
|
||||||
function onPacket(packet: ClientboundNotification) {
|
|
||||||
if (packet.type === "UserSettingsUpdate") {
|
|
||||||
try {
|
|
||||||
state.sync.apply(packet.update);
|
|
||||||
} catch (err) {
|
|
||||||
reportError(err as any, "failed_sync_apply");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.addListener("packet", onPacket);
|
|
||||||
return () => client.removeListener("packet", onPacket);
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
}
|
|
|
@ -1,15 +1,6 @@
|
||||||
import { Client, Server } from "revolt.js";
|
export const _ = "";
|
||||||
|
|
||||||
import { StateUpdater } from "preact/hooks";
|
/*export function registerEvents(
|
||||||
|
|
||||||
import { deleteRenderer } from "../../lib/renderer/Singleton";
|
|
||||||
|
|
||||||
import State from "../../mobx/State";
|
|
||||||
|
|
||||||
import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar";
|
|
||||||
import { ClientStatus } from "./RevoltClient";
|
|
||||||
|
|
||||||
export function registerEvents(
|
|
||||||
state: State,
|
state: State,
|
||||||
setStatus: StateUpdater<ClientStatus>,
|
setStatus: StateUpdater<ClientStatus>,
|
||||||
client: Client,
|
client: Client,
|
||||||
|
@ -86,4 +77,4 @@ export function registerEvents(
|
||||||
window.removeEventListener("online", online);
|
window.removeEventListener("online", online);
|
||||||
window.removeEventListener("offline", offline);
|
window.removeEventListener("offline", offline);
|
||||||
};
|
};
|
||||||
}
|
}*/
|
||||||
|
|
316
src/controllers/client/ClientController.tsx
Normal file
316
src/controllers/client/ClientController.tsx
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
import { detect } from "detect-browser";
|
||||||
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
|
import { API, Client, Nullable } from "revolt.js";
|
||||||
|
|
||||||
|
import { injectController } from "../../lib/window";
|
||||||
|
|
||||||
|
import { state } from "../../mobx/State";
|
||||||
|
import Auth from "../../mobx/stores/Auth";
|
||||||
|
|
||||||
|
import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar";
|
||||||
|
import { modalController } from "../modals/ModalController";
|
||||||
|
import Session from "./Session";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the lifecycles of clients
|
||||||
|
*/
|
||||||
|
class ClientController {
|
||||||
|
/**
|
||||||
|
* API client
|
||||||
|
*/
|
||||||
|
private apiClient: Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server configuration
|
||||||
|
*/
|
||||||
|
private configuration: API.RevoltConfig | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of user IDs to sessions
|
||||||
|
*/
|
||||||
|
private sessions: ObservableMap<string, Session>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User ID of active session
|
||||||
|
*/
|
||||||
|
private current: Nullable<string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.apiClient = new Client({
|
||||||
|
apiURL: import.meta.env.VITE_API_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ! FIXME: loop until success infinitely
|
||||||
|
this.apiClient
|
||||||
|
.fetchConfiguration()
|
||||||
|
.then(() => (this.configuration = this.apiClient.configuration!));
|
||||||
|
|
||||||
|
this.configuration = null;
|
||||||
|
this.sessions = new ObservableMap();
|
||||||
|
this.current = null;
|
||||||
|
|
||||||
|
makeAutoObservable(this);
|
||||||
|
|
||||||
|
this.login = this.login.bind(this);
|
||||||
|
this.logoutCurrent = this.logoutCurrent.bind(this);
|
||||||
|
|
||||||
|
// Inject globally
|
||||||
|
injectController("client", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action pickNextSession() {
|
||||||
|
this.switchAccount(
|
||||||
|
this.current ?? this.sessions.keys().next().value ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrate sessions and start client lifecycles.
|
||||||
|
* @param auth Authentication store
|
||||||
|
*/
|
||||||
|
@action hydrate(auth: Auth) {
|
||||||
|
for (const entry of auth.getAccounts()) {
|
||||||
|
this.addSession(entry, "existing");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pickNextSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently selected session
|
||||||
|
* @returns Active Session
|
||||||
|
*/
|
||||||
|
@computed getActiveSession() {
|
||||||
|
return this.sessions.get(this.current!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently ready client
|
||||||
|
* @returns Ready Client
|
||||||
|
*/
|
||||||
|
@computed getReadyClient() {
|
||||||
|
const session = this.getActiveSession();
|
||||||
|
return session && session.ready ? session.client! : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an unauthenticated instance of the Revolt.js Client
|
||||||
|
* @returns API Client
|
||||||
|
*/
|
||||||
|
@computed getAnonymousClient() {
|
||||||
|
return this.apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next available client (either from session or API)
|
||||||
|
* @returns Revolt.js Client
|
||||||
|
*/
|
||||||
|
@computed getAvailableClient() {
|
||||||
|
return this.getActiveSession()?.client ?? this.apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch server configuration
|
||||||
|
* @returns Server Configuration
|
||||||
|
*/
|
||||||
|
@computed getServerConfig() {
|
||||||
|
return this.configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether we are logged in right now
|
||||||
|
* @returns Whether we are logged in
|
||||||
|
*/
|
||||||
|
@computed isLoggedIn() {
|
||||||
|
return this.current !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether we are currently ready
|
||||||
|
* @returns Whether we are ready to render
|
||||||
|
*/
|
||||||
|
@computed isReady() {
|
||||||
|
return this.getActiveSession()?.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new client lifecycle
|
||||||
|
* @param entry Session Information
|
||||||
|
* @param knowledge Whether the session is new or existing
|
||||||
|
*/
|
||||||
|
@action addSession(
|
||||||
|
entry: { session: SessionPrivate; apiUrl?: string },
|
||||||
|
knowledge: "new" | "existing",
|
||||||
|
) {
|
||||||
|
const user_id = entry.session.user_id!;
|
||||||
|
|
||||||
|
const session = new Session();
|
||||||
|
this.sessions.set(user_id, session);
|
||||||
|
this.pickNextSession();
|
||||||
|
|
||||||
|
session
|
||||||
|
.emit({
|
||||||
|
action: "LOGIN",
|
||||||
|
session: entry.session,
|
||||||
|
apiUrl: entry.apiUrl,
|
||||||
|
configuration: this.configuration!,
|
||||||
|
knowledge,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error === "Forbidden" || error === "Unauthorized") {
|
||||||
|
this.sessions.delete(user_id);
|
||||||
|
state.auth.removeSession(user_id);
|
||||||
|
modalController.push({ type: "signed_out" });
|
||||||
|
session.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login given a set of credentials
|
||||||
|
* @param credentials Credentials
|
||||||
|
*/
|
||||||
|
async login(credentials: API.DataLogin) {
|
||||||
|
const browser = detect();
|
||||||
|
|
||||||
|
// Generate a friendly name for this browser
|
||||||
|
let friendly_name;
|
||||||
|
if (browser) {
|
||||||
|
let { name } = browser;
|
||||||
|
const { os } = browser;
|
||||||
|
let isiPad;
|
||||||
|
if (window.isNative) {
|
||||||
|
friendly_name = `Revolt Desktop on ${os}`;
|
||||||
|
} else {
|
||||||
|
if (name === "ios") {
|
||||||
|
name = "safari";
|
||||||
|
} else if (name === "fxios") {
|
||||||
|
name = "firefox";
|
||||||
|
} else if (name === "crios") {
|
||||||
|
name = "chrome";
|
||||||
|
}
|
||||||
|
if (os === "Mac OS" && navigator.maxTouchPoints > 0)
|
||||||
|
isiPad = true;
|
||||||
|
friendly_name = `${name} on ${isiPad ? "iPadOS" : os}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
friendly_name = "Unknown Device";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to login with given credentials
|
||||||
|
let session = await this.apiClient.api.post("/auth/session/login", {
|
||||||
|
...credentials,
|
||||||
|
friendly_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prompt for MFA verificaiton if necessary
|
||||||
|
if (session.result === "MFA") {
|
||||||
|
const { allowed_methods } = session;
|
||||||
|
while (session.result === "MFA") {
|
||||||
|
const mfa_response: API.MFAResponse | undefined =
|
||||||
|
await new Promise((callback) =>
|
||||||
|
modalController.push({
|
||||||
|
type: "mfa_flow",
|
||||||
|
state: "unknown",
|
||||||
|
available_methods: allowed_methods,
|
||||||
|
callback,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof mfa_response === "undefined") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
session = await this.apiClient.api.post(
|
||||||
|
"/auth/session/login",
|
||||||
|
{
|
||||||
|
mfa_response,
|
||||||
|
mfa_ticket: session.ticket,
|
||||||
|
friendly_name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed login:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.result === "MFA") {
|
||||||
|
throw "Cancelled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start client lifecycle
|
||||||
|
this.addSession(
|
||||||
|
{
|
||||||
|
session,
|
||||||
|
},
|
||||||
|
"new",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log out of a specific user session
|
||||||
|
* @param user_id Target User ID
|
||||||
|
*/
|
||||||
|
@action logout(user_id: string) {
|
||||||
|
const session = this.sessions.get(user_id);
|
||||||
|
if (session) {
|
||||||
|
if (user_id === this.current) {
|
||||||
|
this.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessions.delete(user_id);
|
||||||
|
this.pickNextSession();
|
||||||
|
session.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout of the current session
|
||||||
|
*/
|
||||||
|
@action logoutCurrent() {
|
||||||
|
if (this.current) {
|
||||||
|
this.logout(this.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to another user session
|
||||||
|
* @param user_id Target User ID
|
||||||
|
*/
|
||||||
|
@action switchAccount(user_id: string) {
|
||||||
|
this.current = user_id;
|
||||||
|
|
||||||
|
// This will allow account switching to work more seamlessly,
|
||||||
|
// maybe it'll be properly / fully implemented at some point.
|
||||||
|
resetMemberSidebarFetched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clientController = new ClientController();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently active session.
|
||||||
|
* @returns Session
|
||||||
|
*/
|
||||||
|
export function useSession() {
|
||||||
|
return clientController.getActiveSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently active client or an unauthorised
|
||||||
|
* client for API requests, whichever is available.
|
||||||
|
* @returns Revolt.js Client
|
||||||
|
*/
|
||||||
|
export function useClient() {
|
||||||
|
return clientController.getAvailableClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unauthorised client for API requests.
|
||||||
|
* @returns Revolt.js Client
|
||||||
|
*/
|
||||||
|
export function useApi() {
|
||||||
|
return clientController.getAnonymousClient().api;
|
||||||
|
}
|
277
src/controllers/client/Session.tsx
Normal file
277
src/controllers/client/Session.tsx
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
import { action, computed, makeAutoObservable } from "mobx";
|
||||||
|
import { API, Client } from "revolt.js";
|
||||||
|
|
||||||
|
import { state } from "../../mobx/State";
|
||||||
|
|
||||||
|
import { __thisIsAHack } from "../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
|
import { modalController } from "../modals/ModalController";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current lifecycle state
|
||||||
|
*/
|
||||||
|
type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible transitions between states
|
||||||
|
*/
|
||||||
|
type Transition =
|
||||||
|
| {
|
||||||
|
action: "LOGIN";
|
||||||
|
apiUrl?: string;
|
||||||
|
session: SessionPrivate;
|
||||||
|
configuration?: API.RevoltConfig;
|
||||||
|
|
||||||
|
knowledge: "new" | "existing";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action:
|
||||||
|
| "SUCCESS"
|
||||||
|
| "DISCONNECT"
|
||||||
|
| "RETRY"
|
||||||
|
| "LOGOUT"
|
||||||
|
| "ONLINE"
|
||||||
|
| "OFFLINE";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client lifecycle finite state machine
|
||||||
|
*/
|
||||||
|
export default class Session {
|
||||||
|
state: State = window.navigator.onLine ? "Ready" : "Offline";
|
||||||
|
user_id: string | null = null;
|
||||||
|
client: Client | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Session
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
|
||||||
|
this.onDropped = this.onDropped.bind(this);
|
||||||
|
this.onReady = this.onReady.bind(this);
|
||||||
|
this.onOnline = this.onOnline.bind(this);
|
||||||
|
this.onOffline = this.onOffline.bind(this);
|
||||||
|
|
||||||
|
window.addEventListener("online", this.onOnline);
|
||||||
|
window.addEventListener("offline", this.onOffline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate logout and destroy client
|
||||||
|
*/
|
||||||
|
@action destroy() {
|
||||||
|
if (this.client) {
|
||||||
|
this.client.logout(false);
|
||||||
|
this.state = "Ready";
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user's browser signals it is online
|
||||||
|
*/
|
||||||
|
private onOnline() {
|
||||||
|
this.emit({
|
||||||
|
action: "ONLINE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user's browser signals it is offline
|
||||||
|
*/
|
||||||
|
private onOffline() {
|
||||||
|
this.emit({
|
||||||
|
action: "OFFLINE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the client signals it has disconnected
|
||||||
|
*/
|
||||||
|
private onDropped() {
|
||||||
|
this.emit({
|
||||||
|
action: "DISCONNECT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the client signals it has received the Ready packet
|
||||||
|
*/
|
||||||
|
private onReady() {
|
||||||
|
this.emit({
|
||||||
|
action: "SUCCESS",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Revolt.js Client for this Session
|
||||||
|
* @param apiUrl Optionally specify an API URL
|
||||||
|
*/
|
||||||
|
private createClient(apiUrl?: string) {
|
||||||
|
this.client = new Client({
|
||||||
|
unreads: true,
|
||||||
|
autoReconnect: false,
|
||||||
|
onPongTimeout: "EXIT",
|
||||||
|
apiURL: apiUrl ?? import.meta.env.VITE_API_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.addListener("dropped", this.onDropped);
|
||||||
|
this.client.addListener("ready", this.onReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the client including any listeners.
|
||||||
|
*/
|
||||||
|
private destroyClient() {
|
||||||
|
this.client!.removeAllListeners();
|
||||||
|
this.client!.logout();
|
||||||
|
this.user_id = null;
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure we are in one of the given states
|
||||||
|
* @param state Possible states
|
||||||
|
*/
|
||||||
|
private assert(...state: State[]) {
|
||||||
|
let found = false;
|
||||||
|
for (const target of state) {
|
||||||
|
if (this.state === target) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
throw `State must be ${state} in order to transition! (currently ${this.state})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue logging in provided onboarding is successful
|
||||||
|
* @param data Transition Data
|
||||||
|
*/
|
||||||
|
private async continueLogin(data: Transition & { action: "LOGIN" }) {
|
||||||
|
try {
|
||||||
|
await this.client!.useExistingSession(data.session);
|
||||||
|
this.user_id = this.client!.user!._id;
|
||||||
|
state.auth.setSession(data.session);
|
||||||
|
} catch (err) {
|
||||||
|
this.state = "Ready";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition to a new state by a certain action
|
||||||
|
* @param data Transition Data
|
||||||
|
*/
|
||||||
|
@action async emit(data: Transition) {
|
||||||
|
console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data);
|
||||||
|
|
||||||
|
switch (data.action) {
|
||||||
|
// Login with session
|
||||||
|
case "LOGIN": {
|
||||||
|
this.assert("Ready");
|
||||||
|
this.state = "Connecting";
|
||||||
|
this.createClient(data.apiUrl);
|
||||||
|
|
||||||
|
if (data.configuration) {
|
||||||
|
this.client!.configuration = data.configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.knowledge === "new") {
|
||||||
|
await this.client!.fetchConfiguration();
|
||||||
|
this.client!.session = data.session;
|
||||||
|
(this.client! as any).$updateHeaders();
|
||||||
|
|
||||||
|
const { onboarding } = await this.client!.api.get(
|
||||||
|
"/onboard/hello",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onboarding) {
|
||||||
|
modalController.push({
|
||||||
|
type: "onboarding",
|
||||||
|
callback: async (username: string) =>
|
||||||
|
this.client!.completeOnboarding(
|
||||||
|
{ username },
|
||||||
|
false,
|
||||||
|
).then(() => this.continueLogin(data)),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.continueLogin(data);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Ready successfully received
|
||||||
|
case "SUCCESS": {
|
||||||
|
this.assert("Connecting");
|
||||||
|
this.state = "Online";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Client got disconnected
|
||||||
|
case "DISCONNECT": {
|
||||||
|
if (navigator.onLine) {
|
||||||
|
this.assert("Online");
|
||||||
|
this.state = "Disconnected";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Check we are still disconnected before retrying.
|
||||||
|
if (this.state === "Disconnected") {
|
||||||
|
this.emit({
|
||||||
|
action: "RETRY",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We should try reconnecting
|
||||||
|
case "RETRY": {
|
||||||
|
this.assert("Disconnected");
|
||||||
|
this.client!.websocket.connect();
|
||||||
|
this.state = "Connecting";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// User instructed logout
|
||||||
|
case "LOGOUT": {
|
||||||
|
this.assert("Connecting", "Online", "Disconnected");
|
||||||
|
this.state = "Ready";
|
||||||
|
this.destroyClient();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Browser went offline
|
||||||
|
case "OFFLINE": {
|
||||||
|
this.state = "Offline";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Browser went online
|
||||||
|
case "ONLINE": {
|
||||||
|
this.assert("Offline");
|
||||||
|
if (this.client) {
|
||||||
|
this.state = "Disconnected";
|
||||||
|
this.emit({
|
||||||
|
action: "RETRY",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.state = "Ready";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we are ready to render.
|
||||||
|
* @returns Boolean
|
||||||
|
*/
|
||||||
|
@computed get ready() {
|
||||||
|
return !!this.client?.user;
|
||||||
|
}
|
||||||
|
}
|
18
src/controllers/client/jsx/Binder.tsx
Normal file
18
src/controllers/client/jsx/Binder.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
import { state } from "../../../mobx/State";
|
||||||
|
|
||||||
|
import { clientController } from "../ClientController";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Also binds listeners from state to the current client.
|
||||||
|
*/
|
||||||
|
const Binder: React.FC = () => {
|
||||||
|
const client = clientController.getReadyClient();
|
||||||
|
useEffect(() => state.registerListeners(client!), [client]);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(Binder);
|
|
@ -2,11 +2,10 @@ import { WifiOff } from "@styled-icons/boxicons-regular";
|
||||||
import styled from "styled-components/macro";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { Preloader } from "@revoltchat/ui";
|
import { Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { ClientStatus, StatusContext } from "./RevoltClient";
|
import { useSession } from "../ClientController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Children;
|
children: Children;
|
||||||
|
@ -29,10 +28,12 @@ const Base = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function RequiresOnline(props: Props) {
|
export default function RequiresOnline(props: Props) {
|
||||||
const status = useContext(StatusContext);
|
const session = useSession();
|
||||||
|
|
||||||
if (status === ClientStatus.CONNECTING) return <Preloader type="ring" />;
|
if (!session || session.state === "Connecting")
|
||||||
if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY)
|
return <Preloader type="ring" />;
|
||||||
|
|
||||||
|
if (!(session.state === "Online" || session.state === "Ready"))
|
||||||
return (
|
return (
|
||||||
<Base>
|
<Base>
|
||||||
<WifiOff size={16} />
|
<WifiOff size={16} />
|
|
@ -9,15 +9,18 @@ import type { Client, API } from "revolt.js";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
|
|
||||||
import { determineLink } from "../../lib/links";
|
import { determineLink } from "../../lib/links";
|
||||||
|
import { injectController } from "../../lib/window";
|
||||||
|
|
||||||
import { getApplicationState, useApplicationState } from "../../mobx/State";
|
import { getApplicationState } from "../../mobx/State";
|
||||||
|
|
||||||
|
import { history } from "../../context/history";
|
||||||
|
import { __thisIsAHack } from "../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
import { history } from "../history";
|
|
||||||
import { __thisIsAHack } from "../intermediate/Intermediate";
|
|
||||||
// import { determineLink } from "../../lib/links";
|
|
||||||
import Changelog from "./components/Changelog";
|
import Changelog from "./components/Changelog";
|
||||||
|
import ChannelInfo from "./components/ChannelInfo";
|
||||||
import Clipboard from "./components/Clipboard";
|
import Clipboard from "./components/Clipboard";
|
||||||
import Error from "./components/Error";
|
import Error from "./components/Error";
|
||||||
|
import ImageViewer from "./components/ImageViewer";
|
||||||
import LinkWarning from "./components/LinkWarning";
|
import LinkWarning from "./components/LinkWarning";
|
||||||
import MFAEnableTOTP from "./components/MFAEnableTOTP";
|
import MFAEnableTOTP from "./components/MFAEnableTOTP";
|
||||||
import MFAFlow from "./components/MFAFlow";
|
import MFAFlow from "./components/MFAFlow";
|
||||||
|
@ -26,9 +29,14 @@ import ModifyAccount from "./components/ModifyAccount";
|
||||||
import OutOfDate from "./components/OutOfDate";
|
import OutOfDate from "./components/OutOfDate";
|
||||||
import PendingFriendRequests from "./components/PendingFriendRequests";
|
import PendingFriendRequests from "./components/PendingFriendRequests";
|
||||||
import ServerIdentity from "./components/ServerIdentity";
|
import ServerIdentity from "./components/ServerIdentity";
|
||||||
|
import ServerInfo from "./components/ServerInfo";
|
||||||
import ShowToken from "./components/ShowToken";
|
import ShowToken from "./components/ShowToken";
|
||||||
import SignOutSessions from "./components/SignOutSessions";
|
import SignOutSessions from "./components/SignOutSessions";
|
||||||
import SignedOut from "./components/SignedOut";
|
import SignedOut from "./components/SignedOut";
|
||||||
|
import { UserPicker } from "./components/UserPicker";
|
||||||
|
import { CreateBotModal } from "./components/legacy/CreateBot";
|
||||||
|
import { OnboardingModal } from "./components/legacy/Onboarding";
|
||||||
|
import { UserProfile } from "./components/legacy/UserProfile";
|
||||||
import { Modal } from "./types";
|
import { Modal } from "./types";
|
||||||
|
|
||||||
type Components = Record<string, React.FC<any>>;
|
type Components = Record<string, React.FC<any>>;
|
||||||
|
@ -51,6 +59,11 @@ class ModalController<T extends Modal> {
|
||||||
rendered: computed,
|
rendered: computed,
|
||||||
isVisible: computed,
|
isVisible: computed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.close = this.close.bind(this);
|
||||||
|
|
||||||
|
// Inject globally
|
||||||
|
injectController("modal", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +90,13 @@ class ModalController<T extends Modal> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the top modal
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.pop("close");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the keyed modal from the stack
|
* Remove the keyed modal from the stack
|
||||||
*/
|
*/
|
||||||
|
@ -174,7 +194,7 @@ class ModalControllerExtended extends ModalController<Modal> {
|
||||||
|
|
||||||
switch (link.type) {
|
switch (link.type) {
|
||||||
case "profile": {
|
case "profile": {
|
||||||
__thisIsAHack({ id: "profile", user_id: link.id });
|
this.push({ type: "user_profile", user_id: link.id });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "navigate": {
|
case "navigate": {
|
||||||
|
@ -203,17 +223,24 @@ class ModalControllerExtended extends ModalController<Modal> {
|
||||||
|
|
||||||
export const modalController = new ModalControllerExtended({
|
export const modalController = new ModalControllerExtended({
|
||||||
changelog: Changelog,
|
changelog: Changelog,
|
||||||
|
channel_info: ChannelInfo,
|
||||||
clipboard: Clipboard,
|
clipboard: Clipboard,
|
||||||
|
create_bot: CreateBotModal,
|
||||||
error: Error,
|
error: Error,
|
||||||
|
image_viewer: ImageViewer,
|
||||||
link_warning: LinkWarning,
|
link_warning: LinkWarning,
|
||||||
mfa_flow: MFAFlow,
|
mfa_flow: MFAFlow,
|
||||||
mfa_recovery: MFARecovery,
|
mfa_recovery: MFARecovery,
|
||||||
mfa_enable_totp: MFAEnableTOTP,
|
mfa_enable_totp: MFAEnableTOTP,
|
||||||
modify_account: ModifyAccount,
|
modify_account: ModifyAccount,
|
||||||
|
onboarding: OnboardingModal,
|
||||||
out_of_date: OutOfDate,
|
out_of_date: OutOfDate,
|
||||||
pending_friend_requests: PendingFriendRequests,
|
pending_friend_requests: PendingFriendRequests,
|
||||||
server_identity: ServerIdentity,
|
server_identity: ServerIdentity,
|
||||||
|
server_info: ServerInfo,
|
||||||
show_token: ShowToken,
|
show_token: ShowToken,
|
||||||
signed_out: SignedOut,
|
signed_out: SignedOut,
|
||||||
sign_out_sessions: SignOutSessions,
|
sign_out_sessions: SignOutSessions,
|
||||||
|
user_picker: UserPicker,
|
||||||
|
user_profile: UserProfile,
|
||||||
});
|
});
|
|
@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
import { modalController } from ".";
|
import { modalController } from "./ModalController";
|
||||||
|
|
||||||
export default observer(() => {
|
export default observer(() => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
29
src/controllers/modals/components/ChannelInfo.tsx
Normal file
29
src/controllers/modals/components/ChannelInfo.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { X } from "@styled-icons/boxicons-regular";
|
||||||
|
|
||||||
|
import { Column, H1, IconButton, Modal, Row } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import Markdown from "../../../components/markdown/Markdown";
|
||||||
|
import { modalController } from "../ModalController";
|
||||||
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
|
export default function ChannelInfo({
|
||||||
|
channel,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"channel_info">) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...props}
|
||||||
|
title={
|
||||||
|
<Row centred>
|
||||||
|
<Column grow>
|
||||||
|
<H1>{`#${channel.name}`}</H1>
|
||||||
|
</Column>
|
||||||
|
<IconButton onClick={modalController.close}>
|
||||||
|
<X size={36} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
}>
|
||||||
|
<Markdown content={channel.description!} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,23 +1,40 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
import styled from "styled-components";
|
||||||
import { API } from "revolt.js";
|
|
||||||
|
|
||||||
import styles from "./ImageViewer.module.scss";
|
|
||||||
|
|
||||||
import { Modal } from "@revoltchat/ui";
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
|
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
|
||||||
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
|
||||||
import { useClient } from "../../revoltjs/RevoltClient";
|
import { useClient } from "../../client/ClientController";
|
||||||
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
interface Props {
|
const Viewer = styled.div`
|
||||||
onClose: () => void;
|
display: flex;
|
||||||
embed?: API.Image;
|
overflow: hidden;
|
||||||
attachment?: API.File;
|
flex-direction: column;
|
||||||
}
|
border-end-end-radius: 4px;
|
||||||
|
border-end-start-radius: 4px;
|
||||||
|
|
||||||
type ImageMetadata = API.Metadata & { type: "Image" };
|
max-width: 100vw;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 75vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-bottom: thin solid var(--tertiary-foreground);
|
||||||
|
|
||||||
|
-webkit-touch-callout: default;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function ImageViewer({
|
||||||
|
embed,
|
||||||
|
attachment,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"image_viewer">) {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
export function ImageViewer({ attachment, embed, onClose }: Props) {
|
|
||||||
if (attachment && attachment.metadata.type !== "Image") {
|
if (attachment && attachment.metadata.type !== "Image") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`,
|
`Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`,
|
||||||
|
@ -25,20 +42,16 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClose={onClose} transparent maxHeight="100vh" maxWidth="100vw">
|
<Modal {...props} transparent maxHeight="100vh" maxWidth="100vw">
|
||||||
<div className={styles.viewer}>
|
<Viewer>
|
||||||
{attachment && (
|
{attachment && (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
loading="eager"
|
loading="eager"
|
||||||
src={client.generateFileURL(attachment)}
|
src={client.generateFileURL(attachment)}
|
||||||
width={(attachment.metadata as ImageMetadata).width}
|
width={(attachment.metadata as any).width}
|
||||||
height={
|
height={(attachment.metadata as any).height}
|
||||||
(attachment.metadata as ImageMetadata).height
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<AttachmentActions attachment={attachment} />
|
<AttachmentActions attachment={attachment} />
|
||||||
</>
|
</>
|
||||||
|
@ -54,7 +67,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
|
||||||
<EmbedMediaActions embed={embed} />
|
<EmbedMediaActions embed={embed} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Viewer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -7,8 +7,8 @@ import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { noopTrue } from "../../../lib/js";
|
import { noopTrue } from "../../../lib/js";
|
||||||
|
|
||||||
import { modalController } from "..";
|
|
||||||
import { toConfig } from "../../../components/settings/account/MultiFactorAuthentication";
|
import { toConfig } from "../../../components/settings/account/MultiFactorAuthentication";
|
||||||
|
import { modalController } from "../ModalController";
|
||||||
import { ModalProps } from "../types";
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,17 +1,16 @@
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Category, Error, Modal } from "@revoltchat/ui";
|
import { Category, Error, Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { noopTrue } from "../../../lib/js";
|
import { noopTrue } from "../../../lib/js";
|
||||||
|
|
||||||
import { useApplicationState } from "../../../mobx/State";
|
import { takeError } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
import FormField from "../../../pages/login/FormField";
|
import FormField from "../../../pages/login/FormField";
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
import { useClient } from "../../client/ClientController";
|
||||||
import { takeError } from "../../revoltjs/util";
|
|
||||||
import { ModalProps } from "../types";
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
interface FormInputs {
|
interface FormInputs {
|
||||||
|
@ -29,7 +28,7 @@ export default function ModifyAccount({
|
||||||
field,
|
field,
|
||||||
...props
|
...props
|
||||||
}: ModalProps<"modify_account">) {
|
}: ModalProps<"modify_account">) {
|
||||||
const client = useApplicationState().client!;
|
const client = useClient();
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
|
@ -19,7 +19,8 @@ import {
|
||||||
|
|
||||||
import { noop } from "../../../lib/js";
|
import { noop } from "../../../lib/js";
|
||||||
|
|
||||||
import { FileUploader } from "../../revoltjs/FileUploads";
|
import { FileUploader } from "../../../context/revoltjs/FileUploads";
|
||||||
|
|
||||||
import { ModalProps } from "../types";
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
const Preview = styled(Centred)`
|
const Preview = styled(Centred)`
|
48
src/controllers/modals/components/ServerInfo.tsx
Normal file
48
src/controllers/modals/components/ServerInfo.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { X } from "@styled-icons/boxicons-regular";
|
||||||
|
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
|
||||||
|
import { Column, H1, IconButton, Modal, Row } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import Markdown from "../../../components/markdown/Markdown";
|
||||||
|
import { report } from "../../safety";
|
||||||
|
import { modalController } from "../ModalController";
|
||||||
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
|
export default function ServerInfo({
|
||||||
|
server,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"server_info">) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...props}
|
||||||
|
title={
|
||||||
|
<Row centred>
|
||||||
|
<Column grow>
|
||||||
|
<H1>{server.name}</H1>
|
||||||
|
</Column>
|
||||||
|
<IconButton onClick={modalController.close}>
|
||||||
|
<X size={36} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
onClick: () =>
|
||||||
|
modalController.push({
|
||||||
|
type: "server_identity",
|
||||||
|
member: server.member!,
|
||||||
|
}),
|
||||||
|
children: "Edit Identity",
|
||||||
|
palette: "primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onClick: () => report(server),
|
||||||
|
children: <Text id="app.special.modals.actions.report" />,
|
||||||
|
palette: "error",
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Markdown content={server.description!} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,41 +1,50 @@
|
||||||
import styles from "./UserPicker.module.scss";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useState } from "preact/hooks";
|
import { useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Modal } from "@revoltchat/ui";
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import UserCheckbox from "../../../components/common/user/UserCheckbox";
|
import UserCheckbox from "../../../components/common/user/UserCheckbox";
|
||||||
import { useClient } from "../../revoltjs/RevoltClient";
|
import { useClient } from "../../client/ClientController";
|
||||||
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
interface Props {
|
const List = styled.div`
|
||||||
omit?: string[];
|
max-width: 100%;
|
||||||
onClose: () => void;
|
max-height: 360px;
|
||||||
callback: (users: string[]) => Promise<void>;
|
overflow-y: scroll;
|
||||||
}
|
`;
|
||||||
|
|
||||||
export function UserPicker(props: Props) {
|
export function UserPicker({
|
||||||
|
callback,
|
||||||
|
omit,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"user_picker">) {
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
const omit = [...(props.omit || []), "00000000000000000000000000"];
|
const omitted = useMemo(
|
||||||
|
() => new Set([...(omit || []), "00000000000000000000000000"]),
|
||||||
|
[omit],
|
||||||
|
);
|
||||||
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
{...props}
|
||||||
title={<Text id="app.special.popovers.user_picker.select" />}
|
title={<Text id="app.special.popovers.user_picker.select" />}
|
||||||
onClose={props.onClose}
|
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
children: <Text id="app.special.modals.actions.ok" />,
|
children: <Text id="app.special.modals.actions.ok" />,
|
||||||
onClick: () => props.callback(selected).then(() => true),
|
onClick: () => callback(selected).then(() => true),
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<div className={styles.list}>
|
<List>
|
||||||
{[...client.users.values()]
|
{[...client.users.values()]
|
||||||
.filter(
|
.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x &&
|
x &&
|
||||||
x.relationship === "Friend" &&
|
x.relationship === "Friend" &&
|
||||||
!omit.includes(x._id),
|
!omitted.has(x._id),
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserCheckbox
|
<UserCheckbox
|
||||||
|
@ -53,7 +62,7 @@ export function UserPicker(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</List>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -2,26 +2,29 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { API } from "revolt.js";
|
import { API } from "revolt.js";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Category, Modal } from "@revoltchat/ui";
|
import { Category, Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
import FormField from "../../../pages/login/FormField";
|
import { noopTrue } from "../../../../lib/js";
|
||||||
import { I18nError } from "../../Locale";
|
|
||||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../../revoltjs/util";
|
|
||||||
|
|
||||||
interface Props {
|
import { I18nError } from "../../../../context/Locale";
|
||||||
onClose: () => void;
|
import { takeError } from "../../../../context/revoltjs/util";
|
||||||
onCreate: (bot: API.Bot) => void;
|
|
||||||
}
|
import FormField from "../../../../pages/login/FormField";
|
||||||
|
import { useClient } from "../../../client/ClientController";
|
||||||
|
import { modalController } from "../../ModalController";
|
||||||
|
import { ModalProps } from "../../types";
|
||||||
|
|
||||||
interface FormInputs {
|
interface FormInputs {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreateBotModal({ onClose, onCreate }: Props) {
|
export function CreateBotModal({
|
||||||
const client = useContext(AppContext);
|
onCreate,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"create_bot">) {
|
||||||
|
const client = useClient();
|
||||||
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
@ -29,7 +32,7 @@ export function CreateBotModal({ onClose, onCreate }: Props) {
|
||||||
try {
|
try {
|
||||||
const { bot } = await client.bots.create({ name });
|
const { bot } = await client.bots.create({ name });
|
||||||
onCreate(bot);
|
onCreate(bot);
|
||||||
onClose();
|
modalController.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
}
|
}
|
||||||
|
@ -37,7 +40,7 @@ export function CreateBotModal({ onClose, onCreate }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
onClose={onClose}
|
{...props}
|
||||||
title={<Text id="app.special.popovers.create_bot.title" />}
|
title={<Text id="app.special.popovers.create_bot.title" />}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
|
@ -51,7 +54,7 @@ export function CreateBotModal({ onClose, onCreate }: Props) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
palette: "plain",
|
palette: "plain",
|
||||||
onClick: onClose,
|
onClick: noopTrue,
|
||||||
children: <Text id="app.special.modals.actions.cancel" />,
|
children: <Text id="app.special.modals.actions.cancel" />,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
|
@ -1,5 +1,12 @@
|
||||||
.onboarding {
|
.onboarding {
|
||||||
height: 100vh;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
|
@ -6,21 +6,21 @@ import { useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button, Preloader } from "@revoltchat/ui";
|
import { Button, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { takeError } from "../../../../context/revoltjs/util";
|
||||||
|
|
||||||
import wideSVG from "/assets/wide.svg";
|
import wideSVG from "/assets/wide.svg";
|
||||||
|
|
||||||
import FormField from "../../../pages/login/FormField";
|
import FormField from "../../../../pages/login/FormField";
|
||||||
import { takeError } from "../../revoltjs/util";
|
import { ModalProps } from "../../types";
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void;
|
|
||||||
callback: (username: string, loginAfterSuccess?: true) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormInputs {
|
interface FormInputs {
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingModal({ onClose, callback }: Props) {
|
export function OnboardingModal({
|
||||||
|
callback,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"onboarding">) {
|
||||||
const { handleSubmit, register } = useForm<FormInputs>();
|
const { handleSubmit, register } = useForm<FormInputs>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
@ -28,7 +28,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
|
||||||
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
|
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
callback(username, true)
|
callback(username, true)
|
||||||
.then(() => onClose())
|
.then(() => props.onClose())
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
setError(takeError(err));
|
setError(takeError(err));
|
||||||
setLoading(false);
|
setLoading(false);
|
|
@ -13,7 +13,7 @@ import { UserPermission, API } from "revolt.js";
|
||||||
|
|
||||||
import styles from "./UserProfile.module.scss";
|
import styles from "./UserProfile.module.scss";
|
||||||
import { Localizer, Text } from "preact-i18n";
|
import { Localizer, Text } from "preact-i18n";
|
||||||
import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks";
|
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -24,34 +24,27 @@ import {
|
||||||
Preloader,
|
Preloader,
|
||||||
} from "@revoltchat/ui";
|
} from "@revoltchat/ui";
|
||||||
|
|
||||||
import { noop } from "../../../lib/js";
|
import { noop } from "../../../../lib/js";
|
||||||
|
|
||||||
import ChannelIcon from "../../../components/common/ChannelIcon";
|
import ChannelIcon from "../../../../components/common/ChannelIcon";
|
||||||
import ServerIcon from "../../../components/common/ServerIcon";
|
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||||
import Tooltip from "../../../components/common/Tooltip";
|
import Tooltip from "../../../../components/common/Tooltip";
|
||||||
import UserBadges from "../../../components/common/user/UserBadges";
|
import UserBadges from "../../../../components/common/user/UserBadges";
|
||||||
import UserIcon from "../../../components/common/user/UserIcon";
|
import UserIcon from "../../../../components/common/user/UserIcon";
|
||||||
import { Username } from "../../../components/common/user/UserShort";
|
import { Username } from "../../../../components/common/user/UserShort";
|
||||||
import UserStatus from "../../../components/common/user/UserStatus";
|
import UserStatus from "../../../../components/common/user/UserStatus";
|
||||||
import Markdown from "../../../components/markdown/Markdown";
|
import Markdown from "../../../../components/markdown/Markdown";
|
||||||
import {
|
import { useSession } from "../../../../controllers/client/ClientController";
|
||||||
ClientStatus,
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
StatusContext,
|
import { ModalProps } from "../../types";
|
||||||
useClient,
|
|
||||||
} from "../../revoltjs/RevoltClient";
|
|
||||||
import { useIntermediate } from "../Intermediate";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
user_id: string;
|
|
||||||
dummy?: boolean;
|
|
||||||
onClose?: () => void;
|
|
||||||
dummyProfile?: API.UserProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserProfile = observer(
|
export const UserProfile = observer(
|
||||||
({ user_id, onClose, dummy, dummyProfile }: Props) => {
|
({
|
||||||
const { openScreen, writeClipboard } = useIntermediate();
|
user_id,
|
||||||
|
dummy,
|
||||||
|
dummyProfile,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"user_profile">) => {
|
||||||
const [profile, setProfile] = useState<
|
const [profile, setProfile] = useState<
|
||||||
undefined | null | API.UserProfile
|
undefined | null | API.UserProfile
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
@ -63,13 +56,13 @@ export const UserProfile = observer(
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const client = useClient();
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
const [tab, setTab] = useState("profile");
|
const [tab, setTab] = useState("profile");
|
||||||
|
|
||||||
const user = client.users.get(user_id);
|
const user = client.users.get(user_id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (onClose) useEffect(onClose, []);
|
if (props.onClose) useEffect(props.onClose, []);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,32 +94,26 @@ export const UserProfile = observer(
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dummy) return;
|
if (dummy) return;
|
||||||
if (
|
if (session.state === "Online" && typeof mutual === "undefined") {
|
||||||
status === ClientStatus.ONLINE &&
|
|
||||||
typeof mutual === "undefined"
|
|
||||||
) {
|
|
||||||
setMutual(null);
|
setMutual(null);
|
||||||
user.fetchMutual().then(setMutual);
|
user.fetchMutual().then(setMutual);
|
||||||
}
|
}
|
||||||
}, [mutual, status, dummy, user]);
|
}, [mutual, session.state, dummy, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dummy) return;
|
if (dummy) return;
|
||||||
if (
|
if (session.state === "Online" && typeof profile === "undefined") {
|
||||||
status === ClientStatus.ONLINE &&
|
|
||||||
typeof profile === "undefined"
|
|
||||||
) {
|
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
|
|
||||||
if (user.permission & UserPermission.ViewProfile) {
|
if (user.permission & UserPermission.ViewProfile) {
|
||||||
user.fetchProfile().then(setProfile).catch(noop);
|
user.fetchProfile().then(setProfile).catch(noop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [profile, status, dummy, user]);
|
}, [profile, session.state, dummy, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
status === ClientStatus.ONLINE &&
|
session.state === "Online" &&
|
||||||
user.bot &&
|
user.bot &&
|
||||||
typeof isPublicBot === "undefined"
|
typeof isPublicBot === "undefined"
|
||||||
) {
|
) {
|
||||||
|
@ -136,7 +123,7 @@ export const UserProfile = observer(
|
||||||
.then(() => setIsPublicBot(true))
|
.then(() => setIsPublicBot(true))
|
||||||
.catch(noop);
|
.catch(noop);
|
||||||
}
|
}
|
||||||
}, [isPublicBot, status, user, client.bots]);
|
}, [isPublicBot, session.state, user, client.bots]);
|
||||||
|
|
||||||
const backgroundURL =
|
const backgroundURL =
|
||||||
profile &&
|
profile &&
|
||||||
|
@ -169,8 +156,8 @@ export const UserProfile = observer(
|
||||||
hover={typeof user.avatar !== "undefined"}
|
hover={typeof user.avatar !== "undefined"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
user.avatar &&
|
user.avatar &&
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "image_viewer",
|
type: "image_viewer",
|
||||||
attachment: user.avatar,
|
attachment: user.avatar,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -180,7 +167,7 @@ export const UserProfile = observer(
|
||||||
<span
|
<span
|
||||||
className={styles.username}
|
className={styles.username}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
writeClipboard(user.username)
|
modalController.writeText(user.username)
|
||||||
}>
|
}>
|
||||||
@{user.username}
|
@{user.username}
|
||||||
</span>
|
</span>
|
||||||
|
@ -196,7 +183,7 @@ export const UserProfile = observer(
|
||||||
<Button
|
<Button
|
||||||
palette="accent"
|
palette="accent"
|
||||||
compact
|
compact
|
||||||
onClick={onClose}>
|
onClick={props.onClose}>
|
||||||
Add to server
|
Add to server
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -209,7 +196,7 @@ export const UserProfile = observer(
|
||||||
}>
|
}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose?.();
|
props.onClose?.();
|
||||||
history.push(`/open/${user_id}`);
|
history.push(`/open/${user_id}`);
|
||||||
}}>
|
}}>
|
||||||
<Envelope size={30} />
|
<Envelope size={30} />
|
||||||
|
@ -220,7 +207,7 @@ export const UserProfile = observer(
|
||||||
{user.relationship === "User" && !dummy && (
|
{user.relationship === "User" && !dummy && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose?.();
|
props.onClose?.();
|
||||||
history.push(`/settings/profile`);
|
history.push(`/settings/profile`);
|
||||||
}}>
|
}}>
|
||||||
<Edit size={28} />
|
<Edit size={28} />
|
||||||
|
@ -300,8 +287,8 @@ export const UserProfile = observer(
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
user.bot &&
|
user.bot &&
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "profile",
|
type: "user_profile",
|
||||||
user_id: user.bot.owner,
|
user_id: user.bot.owner,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -364,8 +351,8 @@ export const UserProfile = observer(
|
||||||
x && (
|
x && (
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "profile",
|
type: "user_profile",
|
||||||
user_id: x._id,
|
user_id: x._id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -445,7 +432,7 @@ export const UserProfile = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
onClose={onClose}
|
{...props}
|
||||||
nonDismissable={dummy}
|
nonDismissable={dummy}
|
||||||
transparent
|
transparent
|
||||||
maxWidth="560px">
|
maxWidth="560px">
|
|
@ -1,4 +1,4 @@
|
||||||
import { API, Client, User, Member } from "revolt.js";
|
import { API, Client, User, Member, Channel, Server } from "revolt.js";
|
||||||
|
|
||||||
export type Modal = {
|
export type Modal = {
|
||||||
key?: string;
|
key?: string;
|
||||||
|
@ -72,6 +72,41 @@ export type Modal = {
|
||||||
| {
|
| {
|
||||||
type: "signed_out";
|
type: "signed_out";
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "channel_info";
|
||||||
|
channel: Channel;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "server_info";
|
||||||
|
server: Server;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "image_viewer";
|
||||||
|
embed?: API.Image;
|
||||||
|
attachment?: API.File;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "user_picker";
|
||||||
|
omit?: string[];
|
||||||
|
callback: (users: string[]) => Promise<void>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "user_profile";
|
||||||
|
user_id: string;
|
||||||
|
dummy?: boolean;
|
||||||
|
dummyProfile?: API.UserProfile;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "create_bot";
|
||||||
|
onCreate: (bot: API.Bot) => void;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "onboarding";
|
||||||
|
callback: (
|
||||||
|
username: string,
|
||||||
|
loginAfterSuccess?: true,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
|
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
|
16
src/controllers/safety/index.ts
Normal file
16
src/controllers/safety/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Server } from "revolt.js";
|
||||||
|
|
||||||
|
export function report(object: Server) {
|
||||||
|
let type;
|
||||||
|
if (object instanceof Server) {
|
||||||
|
type = "Server";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`mailto:abuse@revolt.chat?subject=${encodeURIComponent(
|
||||||
|
`${type} Report`,
|
||||||
|
)}&body=${encodeURIComponent(
|
||||||
|
`${type} ID: ${object._id}\nWrite more information here!`,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ import {
|
||||||
openContextMenu,
|
openContextMenu,
|
||||||
} from "preact-context-menu";
|
} from "preact-context-menu";
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
|
|
||||||
import { IconButton, LineDivider } from "@revoltchat/ui";
|
import { IconButton, LineDivider } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
@ -28,17 +27,13 @@ import { QueuedMessage } from "../mobx/stores/MessageQueue";
|
||||||
import { NotificationState } from "../mobx/stores/NotificationOptions";
|
import { NotificationState } from "../mobx/stores/NotificationOptions";
|
||||||
|
|
||||||
import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
|
import { Screen, useIntermediate } from "../context/intermediate/Intermediate";
|
||||||
import { modalController } from "../context/modals";
|
|
||||||
import {
|
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
} from "../context/revoltjs/RevoltClient";
|
|
||||||
import { takeError } from "../context/revoltjs/util";
|
import { takeError } from "../context/revoltjs/util";
|
||||||
import CMNotifications from "./contextmenu/CMNotifications";
|
import CMNotifications from "./contextmenu/CMNotifications";
|
||||||
|
|
||||||
import Tooltip from "../components/common/Tooltip";
|
import Tooltip from "../components/common/Tooltip";
|
||||||
import UserStatus from "../components/common/user/UserStatus";
|
import UserStatus from "../components/common/user/UserStatus";
|
||||||
|
import { useSession } from "../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../controllers/modals/ModalController";
|
||||||
import { internalEmit } from "./eventEmitter";
|
import { internalEmit } from "./eventEmitter";
|
||||||
import { getRenderer } from "./renderer/Singleton";
|
import { getRenderer } from "./renderer/Singleton";
|
||||||
|
|
||||||
|
@ -122,12 +117,12 @@ type Action =
|
||||||
// Tip: This should just be split into separate context menus per logical area.
|
// Tip: This should just be split into separate context menus per logical area.
|
||||||
export default function ContextMenus() {
|
export default function ContextMenus() {
|
||||||
const { openScreen, writeClipboard } = useIntermediate();
|
const { openScreen, writeClipboard } = useIntermediate();
|
||||||
const client = useContext(AppContext);
|
const session = useSession()!;
|
||||||
|
const client = session.client!;
|
||||||
const userId = client.user!._id;
|
const userId = client.user!._id;
|
||||||
const status = useContext(StatusContext);
|
|
||||||
const isOnline = status === ClientStatus.ONLINE;
|
|
||||||
const state = useApplicationState();
|
const state = useApplicationState();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const isOnline = session.state === "Online";
|
||||||
|
|
||||||
function contextClick(data?: Action) {
|
function contextClick(data?: Action) {
|
||||||
if (typeof data === "undefined") return;
|
if (typeof data === "undefined") return;
|
||||||
|
@ -319,7 +314,10 @@ export default function ContextMenus() {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "view_profile":
|
case "view_profile":
|
||||||
openScreen({ id: "profile", user_id: data.user._id });
|
modalController.push({
|
||||||
|
type: "user_profile",
|
||||||
|
user_id: data.user._id,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "message_user":
|
case "message_user":
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
import { useMemo } from "preact/hooks";
|
|
||||||
|
|
||||||
import { useApplicationState } from "../mobx/State";
|
|
||||||
|
|
||||||
import { AppContext } from "../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
export default observer(({ children }: { children: Children }) => {
|
|
||||||
const config = useApplicationState().config;
|
|
||||||
const client = useMemo(() => config.createClient(), [config.get()]);
|
|
||||||
return <AppContext.Provider value={client}>{children}</AppContext.Provider>;
|
|
||||||
});
|
|
|
@ -1,8 +1,6 @@
|
||||||
/* 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";
|
import { Channel, Message, Nullable } from "revolt.js";
|
||||||
import { Message } from "revolt.js";
|
|
||||||
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";
|
||||||
|
|
20
src/lib/window.ts
Normal file
20
src/lib/window.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Inject a key into the window's globals.
|
||||||
|
* @param key Key
|
||||||
|
* @param value Value
|
||||||
|
*/
|
||||||
|
export function injectWindow(key: string, value: any) {
|
||||||
|
(window as any)[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject a controller into the global controllers object.
|
||||||
|
* @param key Key
|
||||||
|
* @param value Value
|
||||||
|
*/
|
||||||
|
export function injectController(key: string, value: any) {
|
||||||
|
(window as any).controllers = {
|
||||||
|
...((window as any).controllers ?? {}),
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
// @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, runInAction } from "mobx";
|
import { action, makeAutoObservable, reaction, runInAction } from "mobx";
|
||||||
import { Client } from "revolt.js";
|
import { Client, ClientboundNotification } from "revolt.js";
|
||||||
|
|
||||||
import { reportError } from "../lib/ErrorBoundary";
|
import { reportError } from "../lib/ErrorBoundary";
|
||||||
|
import { injectWindow } from "../lib/window";
|
||||||
|
|
||||||
|
import { clientController } from "../controllers/client/ClientController";
|
||||||
import Persistent from "./interfaces/Persistent";
|
import Persistent from "./interfaces/Persistent";
|
||||||
import Syncable from "./interfaces/Syncable";
|
import Syncable from "./interfaces/Syncable";
|
||||||
import Auth from "./stores/Auth";
|
import Auth from "./stores/Auth";
|
||||||
|
@ -24,6 +26,7 @@ import Sync, { Data as DataSync, SyncKeys } from "./stores/Sync";
|
||||||
|
|
||||||
export const MIGRATIONS = {
|
export const MIGRATIONS = {
|
||||||
REDUX: 1640305719826,
|
REDUX: 1640305719826,
|
||||||
|
MULTI_SERVER_CONFIG: 1656350006152,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,7 +39,10 @@ export default class State {
|
||||||
locale: LocaleOptions;
|
locale: LocaleOptions;
|
||||||
experiments: Experiments;
|
experiments: Experiments;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
config: ServerConfig;
|
/**
|
||||||
|
* DEPRECATED
|
||||||
|
*/
|
||||||
|
private config: ServerConfig;
|
||||||
notifications: NotificationOptions;
|
notifications: NotificationOptions;
|
||||||
queue: MessageQueue;
|
queue: MessageQueue;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
@ -47,8 +53,6 @@ export default class State {
|
||||||
private persistent: [string, Persistent<unknown>][] = [];
|
private persistent: [string, Persistent<unknown>][] = [];
|
||||||
private disabled: Set<string> = new Set();
|
private disabled: Set<string> = new Set();
|
||||||
|
|
||||||
client?: Client;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct new State.
|
* Construct new State.
|
||||||
*/
|
*/
|
||||||
|
@ -60,21 +64,21 @@ export default class State {
|
||||||
this.experiments = new Experiments();
|
this.experiments = new Experiments();
|
||||||
this.layout = new Layout();
|
this.layout = new Layout();
|
||||||
this.config = new ServerConfig();
|
this.config = new ServerConfig();
|
||||||
this.notifications = new NotificationOptions();
|
this.notifications = new NotificationOptions(this);
|
||||||
this.queue = new MessageQueue();
|
this.queue = new MessageQueue();
|
||||||
this.settings = new Settings();
|
this.settings = new Settings();
|
||||||
this.sync = new Sync(this);
|
this.sync = new Sync(this);
|
||||||
this.plugins = new Plugins(this);
|
this.plugins = new Plugins(this);
|
||||||
this.ordering = new Ordering(this);
|
this.ordering = new Ordering(this);
|
||||||
|
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this);
|
||||||
client: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.register();
|
this.register();
|
||||||
this.setDisabled = this.setDisabled.bind(this);
|
this.setDisabled = this.setDisabled.bind(this);
|
||||||
|
this.onPacket = this.onPacket.bind(this);
|
||||||
|
|
||||||
this.client = undefined;
|
// Inject into window
|
||||||
|
injectWindow("state", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -131,6 +135,20 @@ export default class State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume packets from the client.
|
||||||
|
* @param packet Inbound Packet
|
||||||
|
*/
|
||||||
|
@action onPacket(packet: ClientboundNotification) {
|
||||||
|
if (packet.type === "UserSettingsUpdate") {
|
||||||
|
try {
|
||||||
|
this.sync.apply(packet.update);
|
||||||
|
} catch (err) {
|
||||||
|
reportError(err as any, "failed_sync_apply");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register reaction listeners for persistent data stores.
|
* Register reaction listeners for persistent data stores.
|
||||||
* @returns Function to dispose of listeners
|
* @returns Function to dispose of listeners
|
||||||
|
@ -138,8 +156,28 @@ export default class State {
|
||||||
registerListeners(client?: Client) {
|
registerListeners(client?: Client) {
|
||||||
// If a client is present currently, expose it and provide it to plugins.
|
// If a client is present currently, expose it and provide it to plugins.
|
||||||
if (client) {
|
if (client) {
|
||||||
this.client = client;
|
// Register message listener for clearing queue.
|
||||||
this.plugins.onClient(client);
|
client.addListener("message", this.queue.onMessage);
|
||||||
|
|
||||||
|
// Register listener for incoming packets.
|
||||||
|
client.addListener("packet", this.onPacket);
|
||||||
|
|
||||||
|
// Register events for notifications.
|
||||||
|
client.addListener("message", this.notifications.onMessage);
|
||||||
|
client.addListener(
|
||||||
|
"user/relationship",
|
||||||
|
this.notifications.onRelationship,
|
||||||
|
);
|
||||||
|
document.addEventListener(
|
||||||
|
"visibilitychange",
|
||||||
|
this.notifications.onVisibilityChange,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync settings from remote server.
|
||||||
|
state.sync
|
||||||
|
.pull(client)
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => state.changelog.checkForUpdates());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register all the listeners required for saving and syncing state.
|
// Register all the listeners required for saving and syncing state.
|
||||||
|
@ -225,8 +263,20 @@ export default class State {
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Stop exposing the client.
|
// Remove any listeners attached to client.
|
||||||
this.client = undefined;
|
if (client) {
|
||||||
|
client.removeListener("message", this.queue.onMessage);
|
||||||
|
client.removeListener("packet", this.onPacket);
|
||||||
|
client.removeListener("message", this.notifications.onMessage);
|
||||||
|
client.removeListener(
|
||||||
|
"user/relationship",
|
||||||
|
this.notifications.onRelationship,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
"visibilitychange",
|
||||||
|
this.notifications.onVisibilityChange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Wipe all listeners.
|
// Wipe all listeners.
|
||||||
listeners.forEach((x) => x());
|
listeners.forEach((x) => x());
|
||||||
|
@ -253,6 +303,9 @@ export default class State {
|
||||||
|
|
||||||
// Post-hydration, init plugins.
|
// Post-hydration, init plugins.
|
||||||
this.plugins.init();
|
this.plugins.init();
|
||||||
|
|
||||||
|
// Push authentication information forwards to client controller.
|
||||||
|
clientController.hydrate(this.auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -263,7 +316,7 @@ export default class State {
|
||||||
this.draft = new Draft();
|
this.draft = new Draft();
|
||||||
this.experiments = new Experiments();
|
this.experiments = new Experiments();
|
||||||
this.layout = new Layout();
|
this.layout = new Layout();
|
||||||
this.notifications = new NotificationOptions();
|
this.notifications = new NotificationOptions(this);
|
||||||
this.queue = new MessageQueue();
|
this.queue = new MessageQueue();
|
||||||
this.settings = new Settings();
|
this.settings = new Settings();
|
||||||
this.sync = new Sync(this);
|
this.sync = new Sync(this);
|
||||||
|
@ -277,13 +330,7 @@ export default class State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let state: State;
|
export const state = new State();
|
||||||
|
|
||||||
export async function hydrateState() {
|
|
||||||
state = new State();
|
|
||||||
(window as any).state = state;
|
|
||||||
await state.hydrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the application state
|
* Get the application state
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
import { API } from "revolt.js";
|
|
||||||
import { Nullable } from "revolt.js";
|
|
||||||
|
|
||||||
import { mapToRecord } from "../../lib/conversion";
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
|
import { clientController } from "../../controllers/client/ClientController";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
session: Session;
|
session: Session;
|
||||||
|
apiUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
sessions: Record<string, Account>;
|
sessions: Record<string, Account>;
|
||||||
current?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,14 +21,12 @@ export interface Data {
|
||||||
*/
|
*/
|
||||||
export default class Auth implements Store, Persistent<Data> {
|
export default class Auth implements Store, Persistent<Data> {
|
||||||
private sessions: ObservableMap<string, Account>;
|
private sessions: ObservableMap<string, Account>;
|
||||||
private current: Nullable<string>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct new Auth store.
|
* Construct new Auth store.
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sessions = new ObservableMap();
|
this.sessions = new ObservableMap();
|
||||||
this.current = null;
|
|
||||||
|
|
||||||
// Inject session token if it is provided.
|
// Inject session token if it is provided.
|
||||||
if (import.meta.env.VITE_SESSION_TOKEN) {
|
if (import.meta.env.VITE_SESSION_TOKEN) {
|
||||||
|
@ -40,8 +37,6 @@ export default class Auth implements Store, Persistent<Data> {
|
||||||
token: import.meta.env.VITE_SESSION_TOKEN as string,
|
token: import.meta.env.VITE_SESSION_TOKEN as string,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.current = "0";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
@ -54,7 +49,6 @@ export default class Auth implements Store, Persistent<Data> {
|
||||||
@action toJSON() {
|
@action toJSON() {
|
||||||
return {
|
return {
|
||||||
sessions: JSON.parse(JSON.stringify(mapToRecord(this.sessions))),
|
sessions: JSON.parse(JSON.stringify(mapToRecord(this.sessions))),
|
||||||
current: this.current ?? undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,19 +66,15 @@ export default class Auth implements Store, Persistent<Data> {
|
||||||
this.sessions.set(id, v[id]),
|
this.sessions.set(id, v[id]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.current && this.sessions.has(data.current)) {
|
|
||||||
this.current = data.current;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new session to the auth manager.
|
* Add a new session to the auth manager.
|
||||||
* @param session Session
|
* @param session Session
|
||||||
|
* @param apiUrl Custom API URL
|
||||||
*/
|
*/
|
||||||
@action setSession(session: Session) {
|
@action setSession(session: Session, apiUrl?: string) {
|
||||||
this.sessions.set(session.user_id, { session });
|
this.sessions.set(session.user_id, { session, apiUrl });
|
||||||
this.current = session.user_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,34 +82,39 @@ export default class Auth implements Store, Persistent<Data> {
|
||||||
* @param user_id User ID tied to session
|
* @param user_id User ID tied to session
|
||||||
*/
|
*/
|
||||||
@action removeSession(user_id: string) {
|
@action removeSession(user_id: string) {
|
||||||
if (user_id == this.current) {
|
this.sessions.delete(user_id);
|
||||||
this.current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sessions.delete(user_id);
|
/**
|
||||||
|
* Get all known accounts.
|
||||||
|
* @returns Array of accounts
|
||||||
|
*/
|
||||||
|
@computed getAccounts() {
|
||||||
|
return [...this.sessions.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove current session.
|
* Remove current session.
|
||||||
*/
|
*/
|
||||||
@action logout() {
|
/*@action logout() {
|
||||||
this.current && this.removeSession(this.current);
|
this.current && this.removeSession(this.current);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current session.
|
* Get current session.
|
||||||
* @returns Current session
|
* @returns Current session
|
||||||
*/
|
*/
|
||||||
@computed getSession() {
|
/*@computed getSession() {
|
||||||
if (!this.current) return;
|
if (!this.current) return;
|
||||||
return this.sessions.get(this.current)!.session;
|
return this.sessions.get(this.current)!.session;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether we are currently logged in.
|
* Check whether we are currently logged in.
|
||||||
* @returns Whether we are logged in
|
* @returns Whether we are logged in
|
||||||
*/
|
*/
|
||||||
@computed isLoggedIn() {
|
@computed isLoggedIn() {
|
||||||
return this.current !== null;
|
// ! FIXME: temp proxy info
|
||||||
|
return clientController.getActiveSession()?.ready;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { action, makeAutoObservable, runInAction } from "mobx";
|
import { action, makeAutoObservable, runInAction } from "mobx";
|
||||||
|
|
||||||
import { modalController } from "../../context/modals";
|
|
||||||
|
|
||||||
import { latestChangelog } from "../../assets/changelogs";
|
import { latestChangelog } from "../../assets/changelogs";
|
||||||
|
import { modalController } from "../../controllers/modals/ModalController";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
import Syncable from "../interfaces/Syncable";
|
import Syncable from "../interfaces/Syncable";
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
makeAutoObservable,
|
makeAutoObservable,
|
||||||
observable,
|
observable,
|
||||||
} from "mobx";
|
} from "mobx";
|
||||||
|
import { Message } from "revolt.js";
|
||||||
|
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
|
|
||||||
|
@ -47,6 +48,8 @@ export default class MessageQueue implements Store {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.messages = observable.array([]);
|
this.messages = observable.array([]);
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
|
@ -105,4 +108,16 @@ export default class MessageQueue implements Store {
|
||||||
@computed get(channel: string) {
|
@computed get(channel: string) {
|
||||||
return this.messages.filter((x) => x.channel === channel);
|
return this.messages.filter((x) => x.channel === channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming Message
|
||||||
|
* @param message Message
|
||||||
|
*/
|
||||||
|
@action onMessage(message: Message) {
|
||||||
|
if (!message.nonce) return;
|
||||||
|
if (!this.get(message.channel_id).find((x) => x.id === message.nonce))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.remove(message.nonce);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
|
||||||
import { Channel, Message, Server } from "revolt.js";
|
import { Channel, Message, Server, User } from "revolt.js";
|
||||||
|
import { decodeTime } from "ulid";
|
||||||
|
|
||||||
|
import { translate } from "preact-i18n";
|
||||||
|
|
||||||
import { mapToRecord } from "../../lib/conversion";
|
import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
|
import { history, routeInformation } from "../../context/history";
|
||||||
|
|
||||||
|
import State from "../State";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
import Syncable from "../interfaces/Syncable";
|
import Syncable from "../interfaces/Syncable";
|
||||||
|
@ -37,22 +43,54 @@ export interface Data {
|
||||||
channel?: Record<string, NotificationState>;
|
channel?: Record<string, NotificationState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a notification either directly or using service worker.
|
||||||
|
* @param title Notification Title
|
||||||
|
* @param options Notification Options
|
||||||
|
* @returns Notification
|
||||||
|
*/
|
||||||
|
async function createNotification(
|
||||||
|
title: string,
|
||||||
|
options: globalThis.NotificationOptions,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return new Notification(title, options);
|
||||||
|
} catch (err) {
|
||||||
|
const sw = await navigator.serviceWorker.getRegistration();
|
||||||
|
sw?.showNotification(title, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the user's notification preferences.
|
* Manages the user's notification preferences.
|
||||||
*/
|
*/
|
||||||
export default class NotificationOptions
|
export default class NotificationOptions
|
||||||
implements Store, Persistent<Data>, Syncable
|
implements Store, Persistent<Data>, Syncable
|
||||||
{
|
{
|
||||||
|
private state: State;
|
||||||
|
private activeNotifications: Record<string, Notification>;
|
||||||
|
|
||||||
private server: ObservableMap<string, NotificationState>;
|
private server: ObservableMap<string, NotificationState>;
|
||||||
private channel: ObservableMap<string, NotificationState>;
|
private channel: ObservableMap<string, NotificationState>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct new Experiments store.
|
* Construct new Experiments store.
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor(state: State) {
|
||||||
this.server = new ObservableMap();
|
this.server = new ObservableMap();
|
||||||
this.channel = new ObservableMap();
|
this.channel = new ObservableMap();
|
||||||
makeAutoObservable(this);
|
|
||||||
|
makeAutoObservable(this, {
|
||||||
|
onMessage: false,
|
||||||
|
onRelationship: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = state;
|
||||||
|
this.activeNotifications = {};
|
||||||
|
|
||||||
|
this.onMessage = this.onMessage.bind(this);
|
||||||
|
this.onRelationship = this.onRelationship.bind(this);
|
||||||
|
this.onVisibilityChange = this.onVisibilityChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
|
@ -209,6 +247,245 @@ export default class NotificationOptions
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming messages and create a notification.
|
||||||
|
* @param message Message
|
||||||
|
*/
|
||||||
|
async onMessage(message: Message) {
|
||||||
|
// Ignore if we are currently looking and focused on the channel.
|
||||||
|
if (
|
||||||
|
message.channel_id === routeInformation.getChannel() &&
|
||||||
|
document.hasFocus()
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ignore if muted.
|
||||||
|
if (!this.shouldNotify(message)) return;
|
||||||
|
|
||||||
|
// Play a sound and skip notif if disabled.
|
||||||
|
this.state.settings.sounds.playSound("message");
|
||||||
|
if (!this.state.settings.get("notifications:desktop")) return;
|
||||||
|
|
||||||
|
const effectiveName =
|
||||||
|
message.masquerade?.name ?? message.author?.username;
|
||||||
|
|
||||||
|
let title;
|
||||||
|
switch (message.channel?.channel_type) {
|
||||||
|
case "SavedMessages":
|
||||||
|
return;
|
||||||
|
case "DirectMessage":
|
||||||
|
title = `@${effectiveName}`;
|
||||||
|
break;
|
||||||
|
case "Group":
|
||||||
|
if (message.author?._id === "00000000000000000000000000") {
|
||||||
|
title = message.channel.name;
|
||||||
|
} else {
|
||||||
|
title = `@${effectiveName} - ${message.channel.name}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "TextChannel":
|
||||||
|
title = `@${effectiveName} (#${message.channel.name}, ${message.channel.server?.name})`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = message.channel?._id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image;
|
||||||
|
if (message.attachments) {
|
||||||
|
const imageAttachment = message.attachments.find(
|
||||||
|
(x) => x.metadata.type === "Image",
|
||||||
|
);
|
||||||
|
if (imageAttachment) {
|
||||||
|
image = message.client.generateFileURL(imageAttachment, {
|
||||||
|
max_side: 720,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body, icon;
|
||||||
|
if (message.content) {
|
||||||
|
body = message.client.markdownToText(message.content);
|
||||||
|
|
||||||
|
if (message.masquerade?.avatar) {
|
||||||
|
icon = message.client.proxyFile(message.masquerade.avatar);
|
||||||
|
} else {
|
||||||
|
icon = message.author?.generateAvatarURL({ max_side: 256 });
|
||||||
|
}
|
||||||
|
} else if (message.system) {
|
||||||
|
const users = message.client.users;
|
||||||
|
|
||||||
|
// ! FIXME: I've had to strip translations while
|
||||||
|
// ! I move stuff into the new project structure
|
||||||
|
switch (message.system.type) {
|
||||||
|
case "user_added":
|
||||||
|
case "user_remove":
|
||||||
|
{
|
||||||
|
const user = users.get(message.system.id);
|
||||||
|
body = `${user?.username} ${
|
||||||
|
message.system.type === "user_added"
|
||||||
|
? "added by"
|
||||||
|
: "removed by"
|
||||||
|
} ${users.get(message.system.by)?.username}`;
|
||||||
|
/*body = translate(
|
||||||
|
`app.main.channel.system.${
|
||||||
|
message.system.type === "user_added"
|
||||||
|
? "added_by"
|
||||||
|
: "removed_by"
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
user: user?.username,
|
||||||
|
other_user: users.get(message.system.by)
|
||||||
|
?.username,
|
||||||
|
},
|
||||||
|
);*/
|
||||||
|
icon = user?.generateAvatarURL({
|
||||||
|
max_side: 256,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "user_joined":
|
||||||
|
case "user_left":
|
||||||
|
case "user_kicked":
|
||||||
|
case "user_banned":
|
||||||
|
{
|
||||||
|
const user = users.get(message.system.id);
|
||||||
|
body = `${user?.username}`;
|
||||||
|
/*body = translate(
|
||||||
|
`app.main.channel.system.${message.system.type}`,
|
||||||
|
{ user: user?.username },
|
||||||
|
);*/
|
||||||
|
icon = user?.generateAvatarURL({
|
||||||
|
max_side: 256,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "channel_renamed":
|
||||||
|
{
|
||||||
|
const user = users.get(message.system.by);
|
||||||
|
body = `${user?.username} renamed channel to ${message.system.name}`;
|
||||||
|
/*body = translate(
|
||||||
|
`app.main.channel.system.channel_renamed`,
|
||||||
|
{
|
||||||
|
user: users.get(message.system.by)?.username,
|
||||||
|
name: message.system.name,
|
||||||
|
},
|
||||||
|
);*/
|
||||||
|
icon = user?.generateAvatarURL({
|
||||||
|
max_side: 256,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "channel_description_changed":
|
||||||
|
case "channel_icon_changed":
|
||||||
|
{
|
||||||
|
const user = users.get(message.system.by);
|
||||||
|
/*body = translate(
|
||||||
|
`app.main.channel.system.${message.system.type}`,
|
||||||
|
{ user: users.get(message.system.by)?.username },
|
||||||
|
);*/
|
||||||
|
body = `${users.get(message.system.by)?.username}`;
|
||||||
|
icon = user?.generateAvatarURL({
|
||||||
|
max_side: 256,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notif = await createNotification(title!, {
|
||||||
|
icon,
|
||||||
|
image,
|
||||||
|
body,
|
||||||
|
timestamp: decodeTime(message._id),
|
||||||
|
tag: message.channel?._id,
|
||||||
|
badge: "/assets/icons/android-chrome-512x512.png",
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notif) {
|
||||||
|
notif.addEventListener("click", () => {
|
||||||
|
window.focus();
|
||||||
|
|
||||||
|
const id = message.channel_id;
|
||||||
|
if (id !== routeInformation.getChannel()) {
|
||||||
|
const channel = message.client.channels.get(id);
|
||||||
|
if (channel) {
|
||||||
|
if (channel.channel_type === "TextChannel") {
|
||||||
|
history.push(
|
||||||
|
`/server/${channel.server_id}/channel/${id}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
history.push(`/channel/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeNotifications[message.channel_id] = notif;
|
||||||
|
|
||||||
|
notif.addEventListener(
|
||||||
|
"close",
|
||||||
|
() => delete this.activeNotifications[message.channel_id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle user relationship changes.
|
||||||
|
* @param user User relationship changed with
|
||||||
|
*/
|
||||||
|
async onRelationship(user: User) {
|
||||||
|
// Ignore if disabled.
|
||||||
|
if (!this.state.settings.get("notifications:desktop")) return;
|
||||||
|
|
||||||
|
// Check whether we are busy.
|
||||||
|
// This is checked by `shouldNotify` in the case of messages.
|
||||||
|
if (user.status?.presence === "Busy") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let event;
|
||||||
|
switch (user.relationship) {
|
||||||
|
case "Incoming":
|
||||||
|
/*event = translate("notifications.sent_request", {
|
||||||
|
person: user.username,
|
||||||
|
});*/
|
||||||
|
event = `${user.username} sent you a friend request`;
|
||||||
|
break;
|
||||||
|
case "Friend":
|
||||||
|
/*event = translate("notifications.now_friends", {
|
||||||
|
person: user.username,
|
||||||
|
});*/
|
||||||
|
event = `Now friends with ${user.username}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notif = await createNotification(event, {
|
||||||
|
icon: user.generateAvatarURL({ max_side: 256 }),
|
||||||
|
badge: "/assets/icons/android-chrome-512x512.png",
|
||||||
|
timestamp: +new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
notif?.addEventListener("click", () => {
|
||||||
|
history.push(`/friends`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when document visibility changes.
|
||||||
|
*/
|
||||||
|
onVisibilityChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
const channel_id = routeInformation.getChannel()!;
|
||||||
|
if (this.activeNotifications[channel_id]) {
|
||||||
|
this.activeNotifications[channel_id].close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action apply(_key: "notifications", data: unknown, _revision: number) {
|
@action apply(_key: "notifications", data: unknown, _revision: number) {
|
||||||
this.hydrate(data as Data);
|
this.hydrate(data as Data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { action, computed, makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
import { reorder } from "@revoltchat/ui";
|
import { reorder } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import { clientController } from "../../controllers/client/ClientController";
|
||||||
import State from "../State";
|
import State from "../State";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
|
@ -63,18 +64,19 @@ export default class Ordering implements Store, Persistent<Data>, Syncable {
|
||||||
* All known servers with ordering applied
|
* All known servers with ordering applied
|
||||||
*/
|
*/
|
||||||
@computed get orderedServers() {
|
@computed get orderedServers() {
|
||||||
const known = new Set(this.state.client?.servers.keys() ?? []);
|
const client = clientController.getReadyClient();
|
||||||
|
const known = new Set(client?.servers.keys() ?? []);
|
||||||
const ordered = [...this.servers];
|
const ordered = [...this.servers];
|
||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const id of ordered) {
|
for (const id of ordered) {
|
||||||
if (known.delete(id)) {
|
if (known.delete(id)) {
|
||||||
out.push(this.state.client!.servers.get(id)!);
|
out.push(client!.servers.get(id)!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const id of known) {
|
for (const id of known) {
|
||||||
out.push(this.state.client!.servers.get(id)!);
|
out.push(client!.servers.get(id)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|
|
@ -41,7 +41,6 @@ type Plugin = {
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* function (state: State) {
|
* function (state: State) {
|
||||||
* return {
|
* return {
|
||||||
* onClient: (client: Client) => {},
|
|
||||||
* onUnload: () => {}
|
* onUnload: () => {}
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
|
@ -59,7 +58,6 @@ type Plugin = {
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
format: 1;
|
format: 1;
|
||||||
onClient?: (client: Client) => {};
|
|
||||||
onUnload?: () => void;
|
onUnload?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,7 +122,7 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
* @param id Plugin Id
|
* @param id Plugin Id
|
||||||
*/
|
*/
|
||||||
@computed get(namespace: string, id: string) {
|
@computed get(namespace: string, id: string) {
|
||||||
return this.plugins.get(`${namespace }/${ id}`);
|
return this.plugins.get(`${namespace}/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,7 +131,7 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
* @returns Plugin Instance
|
* @returns Plugin Instance
|
||||||
*/
|
*/
|
||||||
private getInstance(plugin: Pick<Plugin, "namespace" | "id">) {
|
private getInstance(plugin: Pick<Plugin, "namespace" | "id">) {
|
||||||
return this.instances.get(`${plugin.namespace }/${ plugin.id}`);
|
return this.instances.get(`${plugin.namespace}/${plugin.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -159,7 +157,7 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
this.unload(plugin.namespace, plugin.id);
|
this.unload(plugin.namespace, plugin.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.plugins.set(`${plugin.namespace }/${ plugin.id}`, plugin);
|
this.plugins.set(`${plugin.namespace}/${plugin.id}`, plugin);
|
||||||
|
|
||||||
if (typeof plugin.enabled === "undefined" || plugin) {
|
if (typeof plugin.enabled === "undefined" || plugin) {
|
||||||
this.load(plugin.namespace, plugin.id);
|
this.load(plugin.namespace, plugin.id);
|
||||||
|
@ -173,7 +171,7 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
*/
|
*/
|
||||||
remove(namespace: string, id: string) {
|
remove(namespace: string, id: string) {
|
||||||
this.unload(namespace, id);
|
this.unload(namespace, id);
|
||||||
this.plugins.delete(`${namespace }/${ id}`);
|
this.plugins.delete(`${namespace}/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -186,7 +184,7 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
if (!plugin) throw "Unknown plugin!";
|
if (!plugin) throw "Unknown plugin!";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ns = `${plugin.namespace }/${ plugin.id}`;
|
const ns = `${plugin.namespace}/${plugin.id}`;
|
||||||
|
|
||||||
const instance: Instance = eval(plugin.entrypoint)();
|
const instance: Instance = eval(plugin.entrypoint)();
|
||||||
this.instances.set(ns, {
|
this.instances.set(ns, {
|
||||||
|
@ -198,10 +196,6 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
...plugin,
|
...plugin,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.state.client) {
|
|
||||||
instance.onClient?.(this.state.client);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load ${namespace}/${id}!`);
|
console.error(`Failed to load ${namespace}/${id}!`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -217,7 +211,7 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
const plugin = this.get(namespace, id);
|
const plugin = this.get(namespace, id);
|
||||||
if (!plugin) throw "Unknown plugin!";
|
if (!plugin) throw "Unknown plugin!";
|
||||||
|
|
||||||
const ns = `${plugin.namespace }/${ plugin.id}`;
|
const ns = `${plugin.namespace}/${plugin.id}`;
|
||||||
const loaded = this.getInstance(plugin);
|
const loaded = this.getInstance(plugin);
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
loaded.onUnload?.();
|
loaded.onUnload?.();
|
||||||
|
@ -235,13 +229,4 @@ export default class Plugins implements Store, Persistent<Data> {
|
||||||
localforage.removeItem("revite:plugins");
|
localforage.removeItem("revite:plugins");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Push client through to plugins
|
|
||||||
*/
|
|
||||||
onClient(client: Client) {
|
|
||||||
for (const instance of this.instances.values()) {
|
|
||||||
instance.onClient?.(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { action, computed, makeAutoObservable } from "mobx";
|
import { action, computed, makeAutoObservable } from "mobx";
|
||||||
import { API } from "revolt.js";
|
import { API, Client, Nullable } from "revolt.js";
|
||||||
import { Client } from "revolt.js";
|
|
||||||
import { Nullable } from "revolt.js";
|
|
||||||
|
|
||||||
import { isDebug } from "../../revision";
|
import { isDebug } from "../../revision";
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
|
|
|
@ -4,8 +4,7 @@ import { mapToRecord } from "../../lib/conversion";
|
||||||
|
|
||||||
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
|
import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme";
|
||||||
|
|
||||||
import { EmojiPack } from "../../components/common/Emoji";
|
import { EmojiPack, setGlobalEmojiPack } from "../../components/common/Emoji";
|
||||||
import { MIGRATIONS } from "../State";
|
|
||||||
import Persistent from "../interfaces/Persistent";
|
import Persistent from "../interfaces/Persistent";
|
||||||
import Store from "../interfaces/Store";
|
import Store from "../interfaces/Store";
|
||||||
import Syncable from "../interfaces/Syncable";
|
import Syncable from "../interfaces/Syncable";
|
||||||
|
@ -79,6 +78,11 @@ export default class Settings
|
||||||
* @param value Value
|
* @param value Value
|
||||||
*/
|
*/
|
||||||
@action set<T extends keyof ISettings>(key: T, value: ISettings[T]) {
|
@action set<T extends keyof ISettings>(key: T, value: ISettings[T]) {
|
||||||
|
// Emoji needs to be immediately applied.
|
||||||
|
if (key === 'appearance:emoji') {
|
||||||
|
setGlobalEmojiPack(value as EmojiPack);
|
||||||
|
}
|
||||||
|
|
||||||
this.data.set(key, value);
|
this.data.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,24 +2,20 @@
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useContext, useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
import { Header } from "@revoltchat/ui";
|
import { Header } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { modalController } from "../context/modals";
|
import { useSession } from "../controllers/client/ClientController";
|
||||||
import {
|
import { modalController } from "../controllers/modals/ModalController";
|
||||||
AppContext,
|
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
} from "../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
export default function Open() {
|
export default function Open() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const client = useContext(AppContext);
|
const session = useSession()!;
|
||||||
const status = useContext(StatusContext);
|
const client = session.client!;
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
if (status !== ClientStatus.ONLINE) {
|
if (session.state !== "Online") {
|
||||||
return (
|
return (
|
||||||
<Header palette="primary">
|
<Header palette="primary">
|
||||||
<Text id="general.loading" />
|
<Text id="general.loading" />
|
||||||
|
|
|
@ -8,8 +8,6 @@ import ContextMenus from "../lib/ContextMenus";
|
||||||
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import Popovers from "../context/intermediate/Popovers";
|
import Popovers from "../context/intermediate/Popovers";
|
||||||
import Notifications from "../context/revoltjs/Notifications";
|
|
||||||
import StateMonitor from "../context/revoltjs/StateMonitor";
|
|
||||||
|
|
||||||
import { Titlebar } from "../components/native/Titlebar";
|
import { Titlebar } from "../components/native/Titlebar";
|
||||||
import BottomNavigation from "../components/navigation/BottomNavigation";
|
import BottomNavigation from "../components/navigation/BottomNavigation";
|
||||||
|
@ -77,12 +75,6 @@ const Routes = styled.div.attrs({ "data-component": "routes" })<{
|
||||||
|
|
||||||
background: var(--primary-background);
|
background: var(--primary-background);
|
||||||
|
|
||||||
/*background-color: rgba(
|
|
||||||
var(--primary-background-rgb),
|
|
||||||
max(var(--min-opacity), 0.75)
|
|
||||||
);*/
|
|
||||||
//backdrop-filter: blur(10px);
|
|
||||||
|
|
||||||
${() =>
|
${() =>
|
||||||
isTouchscreenDevice &&
|
isTouchscreenDevice &&
|
||||||
css`
|
css`
|
||||||
|
@ -234,8 +226,6 @@ export default function App() {
|
||||||
</Routes>
|
</Routes>
|
||||||
<ContextMenus />
|
<ContextMenus />
|
||||||
<Popovers />
|
<Popovers />
|
||||||
<Notifications />
|
|
||||||
<StateMonitor />
|
|
||||||
</OverlappingPanels>
|
</OverlappingPanels>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { lazy, Suspense } from "preact/compat";
|
||||||
import { Masks, Preloader } from "@revoltchat/ui";
|
import { Masks, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import ErrorBoundary from "../lib/ErrorBoundary";
|
import ErrorBoundary from "../lib/ErrorBoundary";
|
||||||
import FakeClient from "../lib/FakeClient";
|
|
||||||
|
|
||||||
import Context from "../context";
|
import Context from "../context";
|
||||||
import { CheckAuth } from "../context/revoltjs/CheckAuth";
|
import { CheckAuth } from "../context/revoltjs/CheckAuth";
|
||||||
|
@ -16,29 +15,33 @@ const Login = lazy(() => import("./login/Login"));
|
||||||
const ConfirmDelete = lazy(() => import("./login/ConfirmDelete"));
|
const ConfirmDelete = lazy(() => import("./login/ConfirmDelete"));
|
||||||
const RevoltApp = lazy(() => import("./RevoltApp"));
|
const RevoltApp = lazy(() => import("./RevoltApp"));
|
||||||
|
|
||||||
|
const LoadSuspense: React.FC = ({ children }) => (
|
||||||
|
// @ts-expect-error Typing issue between Preact and Preact.
|
||||||
|
<Suspense fallback={<Preloader type="ring" />}>{children}</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary section="client">
|
<ErrorBoundary section="client">
|
||||||
<Context>
|
<Context>
|
||||||
<Masks />
|
<Masks />
|
||||||
{/*
|
|
||||||
// @ts-expect-error typings mis-match between preact... and preact? */}
|
|
||||||
<Suspense fallback={<Preloader type="spinner" />}>
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login/verify/:token">
|
<Route path="/login/verify/:token">
|
||||||
<Login />
|
<Login />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/login/reset/:token">
|
<Route path="/login/reset/:token">
|
||||||
|
<LoadSuspense>
|
||||||
<Login />
|
<Login />
|
||||||
|
</LoadSuspense>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/delete/:token">
|
<Route path="/delete/:token">
|
||||||
|
<LoadSuspense>
|
||||||
<ConfirmDelete />
|
<ConfirmDelete />
|
||||||
|
</LoadSuspense>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/invite/:code">
|
<Route path="/invite/:code">
|
||||||
<CheckAuth blockRender>
|
<CheckAuth blockRender>
|
||||||
<FakeClient>
|
|
||||||
<Invite />
|
<Invite />
|
||||||
</FakeClient>
|
|
||||||
</CheckAuth>
|
</CheckAuth>
|
||||||
<CheckAuth auth blockRender>
|
<CheckAuth auth blockRender>
|
||||||
<Invite />
|
<Invite />
|
||||||
|
@ -46,16 +49,19 @@ export function App() {
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/login">
|
<Route path="/login">
|
||||||
<CheckAuth>
|
<CheckAuth>
|
||||||
|
<LoadSuspense>
|
||||||
<Login />
|
<Login />
|
||||||
|
</LoadSuspense>
|
||||||
</CheckAuth>
|
</CheckAuth>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<CheckAuth auth>
|
<CheckAuth auth>
|
||||||
|
<LoadSuspense>
|
||||||
<RevoltApp />
|
<RevoltApp />
|
||||||
|
</LoadSuspense>
|
||||||
</CheckAuth>
|
</CheckAuth>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
|
||||||
</Context>
|
</Context>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,8 +16,6 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||||
import { useApplicationState } from "../../mobx/State";
|
import { useApplicationState } from "../../mobx/State";
|
||||||
import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout";
|
import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout";
|
||||||
|
|
||||||
import { useClient } from "../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
import AgeGate from "../../components/common/AgeGate";
|
import AgeGate from "../../components/common/AgeGate";
|
||||||
import MessageBox from "../../components/common/messaging/MessageBox";
|
import MessageBox from "../../components/common/messaging/MessageBox";
|
||||||
import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
|
import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom";
|
||||||
|
@ -25,6 +23,7 @@ import NewMessages from "../../components/common/messaging/bars/NewMessages";
|
||||||
import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
|
import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator";
|
||||||
import RightSidebar from "../../components/navigation/RightSidebar";
|
import RightSidebar from "../../components/navigation/RightSidebar";
|
||||||
import { PageHeader } from "../../components/ui/Header";
|
import { PageHeader } from "../../components/ui/Header";
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import ChannelHeader from "./ChannelHeader";
|
import ChannelHeader from "./ChannelHeader";
|
||||||
import { MessageArea } from "./messaging/MessageArea";
|
import { MessageArea } from "./messaging/MessageArea";
|
||||||
import VoiceHeader from "./voice/VoiceHeader";
|
import VoiceHeader from "./voice/VoiceHeader";
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import { At, Hash } from "@styled-icons/boxicons-regular";
|
import { At, Hash } 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";
|
import { Channel, User } from "revolt.js";
|
||||||
import { User } from "revolt.js";
|
|
||||||
import styled from "styled-components/macro";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||||
|
|
||||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
|
||||||
import { getChannelName } from "../../context/revoltjs/util";
|
import { getChannelName } from "../../context/revoltjs/util";
|
||||||
|
|
||||||
import { useStatusColour } from "../../components/common/user/UserIcon";
|
import { useStatusColour } from "../../components/common/user/UserIcon";
|
||||||
import UserStatus from "../../components/common/user/UserStatus";
|
import UserStatus from "../../components/common/user/UserStatus";
|
||||||
import Markdown from "../../components/markdown/Markdown";
|
import Markdown from "../../components/markdown/Markdown";
|
||||||
import { PageHeader } from "../../components/ui/Header";
|
import { PageHeader } from "../../components/ui/Header";
|
||||||
|
import { modalController } from "../../controllers/modals/ModalController";
|
||||||
import HeaderActions from "./actions/HeaderActions";
|
import HeaderActions from "./actions/HeaderActions";
|
||||||
|
|
||||||
export interface ChannelHeaderProps {
|
export interface ChannelHeaderProps {
|
||||||
|
@ -65,8 +64,6 @@ const Info = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default observer(({ channel }: ChannelHeaderProps) => {
|
export default observer(({ channel }: ChannelHeaderProps) => {
|
||||||
const { openScreen } = useIntermediate();
|
|
||||||
|
|
||||||
const name = getChannelName(channel);
|
const name = getChannelName(channel);
|
||||||
let icon, recipient: User | undefined;
|
let icon, recipient: User | undefined;
|
||||||
switch (channel.channel_type) {
|
switch (channel.channel_type) {
|
||||||
|
@ -114,8 +111,8 @@ export default observer(({ channel }: ChannelHeaderProps) => {
|
||||||
<span
|
<span
|
||||||
className="desc"
|
className="desc"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "channel_info",
|
type: "channel_info",
|
||||||
channel,
|
channel,
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout";
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
import UpdateIndicator from "../../../components/common/UpdateIndicator";
|
import UpdateIndicator from "../../../components/common/UpdateIndicator";
|
||||||
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import { ChannelHeaderProps } from "../ChannelHeader";
|
import { ChannelHeaderProps } from "../ChannelHeader";
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
@ -114,8 +115,8 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) {
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openScreen({
|
modalController.push({
|
||||||
id: "user_picker",
|
type: "user_picker",
|
||||||
omit: channel.recipient_ids!,
|
omit: channel.recipient_ids!,
|
||||||
callback: async (users) => {
|
callback: async (users) => {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
|
|
|
@ -24,12 +24,9 @@ import { getRenderer } from "../../../lib/renderer/Singleton";
|
||||||
import { ScrollState } from "../../../lib/renderer/types";
|
import { ScrollState } from "../../../lib/renderer/types";
|
||||||
|
|
||||||
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
|
import { IntermediateContext } from "../../../context/intermediate/Intermediate";
|
||||||
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
|
||||||
import {
|
|
||||||
ClientStatus,
|
|
||||||
StatusContext,
|
|
||||||
} from "../../../context/revoltjs/RevoltClient";
|
|
||||||
|
|
||||||
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
|
import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline";
|
||||||
import ConversationStart from "./ConversationStart";
|
import ConversationStart from "./ConversationStart";
|
||||||
import MessageRenderer from "./MessageRenderer";
|
import MessageRenderer from "./MessageRenderer";
|
||||||
|
|
||||||
|
@ -65,7 +62,7 @@ export const MESSAGE_AREA_PADDING = 82;
|
||||||
|
|
||||||
export const MessageArea = observer(({ last_id, channel }: Props) => {
|
export const MessageArea = observer(({ last_id, channel }: Props) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const status = useContext(StatusContext);
|
const session = useSession()!;
|
||||||
const { focusTaken } = useContext(IntermediateContext);
|
const { focusTaken } = useContext(IntermediateContext);
|
||||||
|
|
||||||
// ? Required data for message links.
|
// ? Required data for message links.
|
||||||
|
@ -213,8 +210,8 @@ export const MessageArea = observer(({ last_id, channel }: Props) => {
|
||||||
|
|
||||||
// ? If we are waiting for network, try again.
|
// ? If we are waiting for network, try again.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (status) {
|
switch (session.state) {
|
||||||
case ClientStatus.ONLINE:
|
case "Online":
|
||||||
if (renderer.state === "WAITING_FOR_NETWORK") {
|
if (renderer.state === "WAITING_FOR_NETWORK") {
|
||||||
renderer.init();
|
renderer.init();
|
||||||
} else {
|
} else {
|
||||||
|
@ -222,13 +219,13 @@ export const MessageArea = observer(({ last_id, channel }: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ClientStatus.OFFLINE:
|
case "Offline":
|
||||||
case ClientStatus.DISCONNECTED:
|
case "Disconnected":
|
||||||
case ClientStatus.CONNECTING:
|
case "Connecting":
|
||||||
renderer.markStale();
|
renderer.markStale();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [renderer, status]);
|
}, [renderer, session.state]);
|
||||||
|
|
||||||
// ? When the container is scrolled.
|
// ? When the container is scrolled.
|
||||||
// ? Also handle StayAtBottom
|
// ? Also handle StayAtBottom
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue