diff --git a/package.json b/package.json index 7e7091a4..bfbf31e5 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "eslint": "^7.28.0", "eslint-config-preact": "^1.1.4", "eslint-plugin-jsdoc": "^39.3.2", + "eslint-plugin-mobx": "^0.0.8", "eventemitter3": "^4.0.7", "history": "4", "json-stringify-deterministic": "^1.0.2", diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx index 2b7d5196..72c0d885 100644 --- a/src/components/common/AutoComplete.tsx +++ b/src/components/common/AutoComplete.tsx @@ -3,9 +3,8 @@ import styled, { css } from "styled-components/macro"; import { StateUpdater, useState } from "preact/hooks"; -import { useClient } from "../../context/revoltjs/RevoltClient"; - import { emojiDictionary } from "../../assets/emojis"; +import { useClient } from "../../controllers/client/ClientController"; import ChannelIcon from "./ChannelIcon"; import Emoji from "./Emoji"; import UserIcon from "./user/UserIcon"; diff --git a/src/components/common/ChannelIcon.tsx b/src/components/common/ChannelIcon.tsx index 92fafcaa..d11d6633 100644 --- a/src/components/common/ChannelIcon.tsx +++ b/src/components/common/ChannelIcon.tsx @@ -2,12 +2,9 @@ import { Hash, VolumeFull } from "@styled-icons/boxicons-regular"; import { observer } from "mobx-react-lite"; import { Channel } from "revolt.js"; -import { useContext } from "preact/hooks"; - -import { AppContext } from "../../context/revoltjs/RevoltClient"; - import fallback from "./assets/group.png"; +import { useClient } from "../../controllers/client/ClientController"; import { ImageIconBase, IconBaseProps } from "./IconBase"; interface Props extends IconBaseProps { @@ -22,7 +19,7 @@ export default observer( keyof Props | "children" | "as" >, ) => { - const client = useContext(AppContext); + const client = useClient(); const { size, diff --git a/src/components/common/ServerIcon.tsx b/src/components/common/ServerIcon.tsx index 282e9d73..a9fc1913 100644 --- a/src/components/common/ServerIcon.tsx +++ b/src/components/common/ServerIcon.tsx @@ -4,8 +4,7 @@ import styled from "styled-components/macro"; import { useContext } from "preact/hooks"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; - +import { useClient } from "../../controllers/client/ClientController"; import { IconBaseProps, ImageIconBase } from "./IconBase"; interface Props extends IconBaseProps { @@ -34,7 +33,7 @@ export default observer( keyof Props | "children" | "as" >, ) => { - const client = useContext(AppContext); + const client = useClient(); const { target, attachment, size, animate, server_name, ...imgProps } = props; diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index a29238a0..3e15f001 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -14,8 +14,8 @@ import { QueuedMessage } from "../../../mobx/stores/MessageQueue"; import { I18nError } from "../../../context/Locale"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; +import { useClient } from "../../../controllers/client/ClientController"; import Markdown from "../../markdown/Markdown"; import UserIcon from "../user/UserIcon"; import { Username } from "../user/UserShort"; @@ -52,7 +52,7 @@ const Message = observer( queued, hideReply, }: Props) => { - const client = useClient(); + const client = message.client; const user = message.author; const { openScreen } = useIntermediate(); diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 4bb4dd49..86c6cf58 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -29,9 +29,9 @@ import { grabFiles, uploadFile, } from "../../../context/revoltjs/FileUploads"; -import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { takeError } from "../../../context/revoltjs/util"; +import { useClient } from "../../../controllers/client/ClientController"; import { modalController } from "../../../controllers/modals/ModalController"; import AutoComplete, { useAutoComplete } from "../AutoComplete"; import { PermissionTooltip } from "../Tooltip"; @@ -148,7 +148,7 @@ export default observer(({ channel }: Props) => { }); const [typing, setTyping] = useState(false); const [replies, setReplies] = useState([]); - const client = useContext(AppContext); + const client = useClient(); const translate = useTranslation(); const renderer = getRenderer(channel); diff --git a/src/components/common/messaging/attachments/Attachment.tsx b/src/components/common/messaging/attachments/Attachment.tsx index 7e9400dc..de112e4d 100644 --- a/src/components/common/messaging/attachments/Attachment.tsx +++ b/src/components/common/messaging/attachments/Attachment.tsx @@ -3,10 +3,9 @@ import { API } from "revolt.js"; import styles from "./Attachment.module.scss"; import classNames from "classnames"; import { useTriggerEvents } from "preact-context-menu"; -import { useContext, useState } from "preact/hooks"; - -import { AppContext } from "../../../../context/revoltjs/RevoltClient"; +import { useState } from "preact/hooks"; +import { useClient } from "../../../../controllers/client/ClientController"; import AttachmentActions from "./AttachmentActions"; import { SizedGrid } from "./Grid"; import ImageFile from "./ImageFile"; @@ -21,7 +20,7 @@ interface Props { const MAX_ATTACHMENT_WIDTH = 480; export default function Attachment({ attachment, hasContent }: Props) { - const client = useContext(AppContext); + const client = useClient(); const { filename, metadata } = attachment; const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_")); diff --git a/src/components/common/messaging/attachments/AttachmentActions.tsx b/src/components/common/messaging/attachments/AttachmentActions.tsx index f2d1781d..b6e5699d 100644 --- a/src/components/common/messaging/attachments/AttachmentActions.tsx +++ b/src/components/common/messaging/attachments/AttachmentActions.tsx @@ -15,14 +15,14 @@ import { IconButton } from "@revoltchat/ui"; import { determineFileSize } from "../../../../lib/fileSize"; -import { AppContext } from "../../../../context/revoltjs/RevoltClient"; +import { useClient } from "../../../../controllers/client/ClientController"; interface Props { attachment: API.File; } export default function AttachmentActions({ attachment }: Props) { - const client = useContext(AppContext); + const client = useClient(); const { filename, metadata, size } = attachment; const url = client.generateFileURL(attachment); diff --git a/src/components/common/messaging/attachments/ImageFile.tsx b/src/components/common/messaging/attachments/ImageFile.tsx index 252387b2..76723e20 100644 --- a/src/components/common/messaging/attachments/ImageFile.tsx +++ b/src/components/common/messaging/attachments/ImageFile.tsx @@ -5,7 +5,8 @@ import classNames from "classnames"; import { useContext, useState } from "preact/hooks"; import { useIntermediate } from "../../../../context/intermediate/Intermediate"; -import { AppContext } from "../../../../context/revoltjs/RevoltClient"; + +import { useClient } from "../../../../controllers/client/ClientController"; enum ImageLoadingState { Loading, @@ -19,7 +20,7 @@ type Props = JSX.HTMLAttributes & { export default function ImageFile({ attachment, ...props }: Props) { const [loading, setLoading] = useState(ImageLoadingState.Loading); - const client = useContext(AppContext); + const client = useClient(); const { openScreen } = useIntermediate(); const url = client.generateFileURL(attachment)!; diff --git a/src/components/common/messaging/attachments/TextFile.tsx b/src/components/common/messaging/attachments/TextFile.tsx index b119bcae..1dd2940b 100644 --- a/src/components/common/messaging/attachments/TextFile.tsx +++ b/src/components/common/messaging/attachments/TextFile.tsx @@ -3,15 +3,13 @@ import { API } from "revolt.js"; import styles from "./Attachment.module.scss"; 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 RequiresOnline from "../../../../context/revoltjs/RequiresOnline"; -import { - AppContext, - StatusContext, -} from "../../../../context/revoltjs/RevoltClient"; + +import { useClient } from "../../../../controllers/client/ClientController"; interface Props { attachment: API.File; @@ -23,9 +21,8 @@ export default function TextFile({ attachment }: Props) { const [gated, setGated] = useState(attachment.size > 100_000); const [content, setContent] = useState(undefined); const [loading, setLoading] = useState(false); - const status = useContext(StatusContext); - const client = useContext(AppContext); + const client = useClient(); const url = client.generateFileURL(attachment)!; useEffect(() => { @@ -56,7 +53,7 @@ export default function TextFile({ attachment }: Props) { setLoading(false); }); } - }, [content, loading, gated, status, attachment._id, attachment.size, url]); + }, [content, loading, gated, attachment._id, attachment.size, url]); return (
{ - const client = useClient(); + const client = message.client; const { openScreen, writeClipboard } = useIntermediate(); const isAuthor = message.author_id === client.user!._id; diff --git a/src/components/common/messaging/embed/Embed.tsx b/src/components/common/messaging/embed/Embed.tsx index 10201989..306f6441 100644 --- a/src/components/common/messaging/embed/Embed.tsx +++ b/src/components/common/messaging/embed/Embed.tsx @@ -5,8 +5,8 @@ import classNames from "classnames"; import { useContext } from "preact/hooks"; import { useIntermediate } from "../../../../context/intermediate/Intermediate"; -import { useClient } from "../../../../context/revoltjs/RevoltClient"; +import { useClient } from "../../../../controllers/client/ClientController"; import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea"; import Markdown from "../../../markdown/Markdown"; import Attachment from "../attachments/Attachment"; diff --git a/src/components/common/messaging/embed/EmbedInvite.tsx b/src/components/common/messaging/embed/EmbedInvite.tsx index 974f8d71..63700c8c 100644 --- a/src/components/common/messaging/embed/EmbedInvite.tsx +++ b/src/components/common/messaging/embed/EmbedInvite.tsx @@ -1,5 +1,4 @@ import { Group } from "@styled-icons/boxicons-solid"; -import { reaction } from "mobx"; import { observer } from "mobx-react-lite"; import { useHistory } from "react-router-dom"; import { Message, API } from "revolt.js"; @@ -12,14 +11,13 @@ import { Button, Category, Preloader } from "@revoltchat/ui"; import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; import { I18nError } from "../../../../context/Locale"; -import { - AppContext, - ClientStatus, - StatusContext, -} from "../../../../context/revoltjs/RevoltClient"; import { takeError } from "../../../../context/revoltjs/util"; import ServerIcon from "../../../../components/common/ServerIcon"; +import { + useClient, + useSession, +} from "../../../../controllers/client/ClientController"; const EmbedInviteBase = styled.div` width: 400px; @@ -78,8 +76,8 @@ type Props = { export function EmbedInvite({ code }: Props) { const history = useHistory(); - const client = useContext(AppContext); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); const [joinError, setJoinError] = useState(undefined); @@ -90,7 +88,7 @@ export function EmbedInvite({ code }: Props) { useEffect(() => { if ( typeof invite === "undefined" && - (status === ClientStatus.ONLINE || status === ClientStatus.READY) + (session.state === "Online" || session.state === "Ready") ) { client .fetchInvite(code) @@ -99,7 +97,7 @@ export function EmbedInvite({ code }: Props) { ) .catch((err) => setError(takeError(err))); } - }, [client, code, invite, status]); + }, [client, code, invite, session.state]); if (typeof invite === "undefined") { return error ? ( diff --git a/src/components/common/messaging/embed/EmbedMedia.tsx b/src/components/common/messaging/embed/EmbedMedia.tsx index f602318b..a719f0e3 100644 --- a/src/components/common/messaging/embed/EmbedMedia.tsx +++ b/src/components/common/messaging/embed/EmbedMedia.tsx @@ -4,7 +4,8 @@ import { API } from "revolt.js"; import styles from "./Embed.module.scss"; import { useIntermediate } from "../../../../context/intermediate/Intermediate"; -import { useClient } from "../../../../context/revoltjs/RevoltClient"; + +import { useClient } from "../../../../controllers/client/ClientController"; interface Props { embed: API.Embed; diff --git a/src/components/common/user/UserIcon.tsx b/src/components/common/user/UserIcon.tsx index c1eade4a..3c96b9a6 100644 --- a/src/components/common/user/UserIcon.tsx +++ b/src/components/common/user/UserIcon.tsx @@ -6,10 +6,9 @@ import styled, { css } from "styled-components/macro"; import { useApplicationState } from "../../../mobx/State"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - import fallback from "../assets/user.png"; +import { useClient } from "../../../controllers/client/ClientController"; import IconBase, { IconBaseProps } from "../IconBase"; type VoiceStatus = "muted" | "deaf"; @@ -56,7 +55,7 @@ export default observer( keyof Props | "children" | "as" >, ) => { - const client = useApplicationState().client!; + const client = useClient(); const { target, diff --git a/src/components/common/user/UserShort.tsx b/src/components/common/user/UserShort.tsx index 881d763f..5ac03d19 100644 --- a/src/components/common/user/UserShort.tsx +++ b/src/components/common/user/UserShort.tsx @@ -9,8 +9,8 @@ import { Text } from "preact-i18n"; import { internalEmit } from "../../../lib/eventEmitter"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; +import { useClient } from "../../../controllers/client/ClientController"; import UserIcon from "./UserIcon"; const BotBadge = styled.div` diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx index 47787a8e..bcce42b8 100644 --- a/src/components/markdown/Renderer.tsx +++ b/src/components/markdown/Renderer.tsx @@ -15,9 +15,9 @@ import { determineLink } from "../../lib/links"; import { dayjs } from "../../context/Locale"; import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; import { emojiDictionary } from "../../assets/emojis"; +import { useClient } from "../../controllers/client/ClientController"; import { generateEmoji } from "../common/Emoji"; import { MarkdownProps } from "./Markdown"; import Prism from "./prism"; @@ -118,7 +118,7 @@ const RE_CHANNELS = /<#([A-z0-9]{26})>/g; const RE_TIME = //g; export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { - const client = useContext(AppContext); + const client = useClient(); const { openLink } = useIntermediate(); if (typeof content === "undefined") return null; diff --git a/src/components/navigation/BottomNavigation.tsx b/src/components/navigation/BottomNavigation.tsx index 2daf28a7..1a1b44ad 100644 --- a/src/components/navigation/BottomNavigation.tsx +++ b/src/components/navigation/BottomNavigation.tsx @@ -9,8 +9,7 @@ import ConditionalLink from "../../lib/ConditionalLink"; import { useApplicationState } from "../../mobx/State"; -import { useClient } from "../../context/revoltjs/RevoltClient"; - +import { useClient } from "../../controllers/client/ClientController"; import UserIcon from "../common/user/UserIcon"; const Base = styled.div` diff --git a/src/components/navigation/items/ConnectionStatus.tsx b/src/components/navigation/items/ConnectionStatus.tsx index 899b3d8e..517c5b03 100644 --- a/src/components/navigation/items/ConnectionStatus.tsx +++ b/src/components/navigation/items/ConnectionStatus.tsx @@ -1,45 +1,43 @@ +import { observer } from "mobx-react-lite"; + import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; import { Banner } from "@revoltchat/ui"; -import { - ClientStatus, - StatusContext, - useClient, -} from "../../../context/revoltjs/RevoltClient"; +import { useSession } from "../../../controllers/client/ClientController"; -export default function ConnectionStatus() { - const status = useContext(StatusContext); - const client = useClient(); +function ConnectionStatus() { + const session = useSession()!; - if (status === ClientStatus.OFFLINE) { + if (session.state === "Offline") { return ( ); - } else if (status === ClientStatus.DISCONNECTED) { + } else if (session.state === "Disconnected") { return (
- client.websocket.connect()}> + + session.emit({ + action: "RETRY", + }) + }>
); - } else if (status === ClientStatus.CONNECTING) { - return ( - - - - ); - } else if (status === ClientStatus.RECONNECTING) { + } else if (session.state === "Connecting") { return ( ); } + return null; } + +export default observer(ConnectionStatus); diff --git a/src/components/navigation/left/HomeSidebar.tsx b/src/components/navigation/left/HomeSidebar.tsx index 220f542c..9dcfb8e9 100644 --- a/src/components/navigation/left/HomeSidebar.tsx +++ b/src/components/navigation/left/HomeSidebar.tsx @@ -21,10 +21,10 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../../mobx/State"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { AppContext } from "../../../context/revoltjs/RevoltClient"; import placeholderSVG from "../items/placeholder.svg"; +import { useClient } from "../../../controllers/client/ClientController"; import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; import ButtonItem, { ChannelButton } from "../items/ButtonItem"; import ConnectionStatus from "../items/ConnectionStatus"; @@ -46,7 +46,7 @@ const Navbar = styled.div` export default observer(() => { const { pathname } = useLocation(); - const client = useContext(AppContext); + const client = useClient(); const state = useApplicationState(); const { channel: channel_id } = useParams<{ channel: string }>(); const { openScreen } = useIntermediate(); diff --git a/src/components/navigation/left/ServerListSidebar.tsx b/src/components/navigation/left/ServerListSidebar.tsx index 23d2b869..ed703a31 100644 --- a/src/components/navigation/left/ServerListSidebar.tsx +++ b/src/components/navigation/left/ServerListSidebar.tsx @@ -8,7 +8,8 @@ import { ServerList } from "@revoltchat/ui"; import { useApplicationState } from "../../../mobx/State"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; + +import { useClient } from "../../../controllers/client/ClientController"; /** * Server list sidebar shim component diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx index 4a771701..b2b602ff 100644 --- a/src/components/navigation/left/ServerSidebar.tsx +++ b/src/components/navigation/left/ServerSidebar.tsx @@ -14,8 +14,7 @@ import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../../mobx/State"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; - +import { useClient } from "../../../controllers/client/ClientController"; import CollapsibleSection from "../../common/CollapsibleSection"; import ServerHeader from "../../common/ServerHeader"; import { ChannelButton } from "../items/ButtonItem"; diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx index 37333b45..3dbf1f63 100644 --- a/src/components/navigation/right/MemberSidebar.tsx +++ b/src/components/navigation/right/MemberSidebar.tsx @@ -4,14 +4,12 @@ import { observer } from "mobx-react-lite"; import { useParams } from "react-router-dom"; import { Channel, Server, User, API } from "revolt.js"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { - ClientStatus, - StatusContext, + useSession, useClient, -} from "../../../context/revoltjs/RevoltClient"; - +} from "../../../controllers/client/ClientController"; import { GenericSidebarBase } from "../SidebarBase"; import MemberList, { MemberListGroup } from "./MemberList"; @@ -205,18 +203,18 @@ function shouldSkipOffline(id: string) { export const ServerMemberSidebar = observer( ({ channel }: { channel: Channel }) => { - const client = useClient(); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; useEffect(() => { const server_id = channel.server_id!; - if (status === ClientStatus.ONLINE && !FETCHED.has(server_id)) { + if (session.state === "Online" && !FETCHED.has(server_id)) { FETCHED.add(server_id); channel .server!.syncMembers(shouldSkipOffline(server_id)) .catch(() => FETCHED.delete(server_id)); } - }, [status, channel]); + }, [session.state, channel]); const entries = useEntries( channel, diff --git a/src/components/navigation/right/Search.tsx b/src/components/navigation/right/Search.tsx index 13fb3d88..f12cb7d2 100644 --- a/src/components/navigation/right/Search.tsx +++ b/src/components/navigation/right/Search.tsx @@ -7,8 +7,7 @@ import { useEffect, useState } from "preact/hooks"; 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 { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; diff --git a/src/components/settings/account/AccountManagement.tsx b/src/components/settings/account/AccountManagement.tsx index f7ac5843..6c9c5310 100644 --- a/src/components/settings/account/AccountManagement.tsx +++ b/src/components/settings/account/AccountManagement.tsx @@ -2,19 +2,16 @@ import { Block } from "@styled-icons/boxicons-regular"; import { Trash } from "@styled-icons/boxicons-solid"; import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; import { CategoryButton } from "@revoltchat/ui"; import { - LogOutContext, + clientController, useClient, -} from "../../../context/revoltjs/RevoltClient"; - +} from "../../../controllers/client/ClientController"; import { modalController } from "../../../controllers/modals/ModalController"; export default function AccountManagement() { - const logOut = useContext(LogOutContext); const client = useClient(); const callback = (route: "disable" | "delete") => () => @@ -27,7 +24,7 @@ export default function AccountManagement() { "X-MFA-Ticket": ticket.token, }, }) - .then(() => logOut(true)), + .then(clientController.logoutCurrent), ); return ( diff --git a/src/components/settings/account/EditAccount.tsx b/src/components/settings/account/EditAccount.tsx index 929bdfb1..2bc24994 100644 --- a/src/components/settings/account/EditAccount.tsx +++ b/src/components/settings/account/EditAccount.tsx @@ -3,7 +3,7 @@ import { Envelope, Key, Pencil } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; import { Text } from "preact-i18n"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { AccountDetail, @@ -12,27 +12,22 @@ import { HiddenValue, } from "@revoltchat/ui"; -import { - ClientStatus, - StatusContext, - useClient, -} from "../../../context/revoltjs/RevoltClient"; - +import { useSession } from "../../../controllers/client/ClientController"; import { modalController } from "../../../controllers/modals/ModalController"; export default observer(() => { - const client = useClient(); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; const [email, setEmail] = useState("..."); useEffect(() => { - if (email === "..." && status === ClientStatus.ONLINE) { + if (email === "..." && session.state === "Online") { client.api .get("/auth/account/") .then((account) => setEmail(account.email)); } - }, [client, email, status]); + }, [client, email, session.state]); return ( <> diff --git a/src/components/settings/account/MultiFactorAuthentication.tsx b/src/components/settings/account/MultiFactorAuthentication.tsx index d1cca41d..5d794bf8 100644 --- a/src/components/settings/account/MultiFactorAuthentication.tsx +++ b/src/components/settings/account/MultiFactorAuthentication.tsx @@ -3,17 +3,13 @@ import { Lock } from "@styled-icons/boxicons-solid"; import { API } from "revolt.js"; 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 { - ClientStatus, - StatusContext, - useClient, -} from "../../../context/revoltjs/RevoltClient"; import { takeError } from "../../../context/revoltjs/util"; +import { useSession } from "../../../controllers/client/ClientController"; import { modalController } from "../../../controllers/modals/ModalController"; /** @@ -34,8 +30,8 @@ export function toConfig(token: string) { */ export default function MultiFactorAuthentication() { // Pull in prerequisites - const client = useClient(); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; // Keep track of MFA state const [mfa, setMFA] = useState(); @@ -43,13 +39,13 @@ export default function MultiFactorAuthentication() { // Fetch the current MFA status on account useEffect(() => { - if (!mfa && status === ClientStatus.ONLINE) { - client.api + if (!mfa && session.state === "Online") { + client!.api .get("/auth/mfa/") .then(setMFA) .catch((err) => setError(takeError(err))); } - }, [client, mfa, status]); + }, [mfa, client, session.state]); // Action called when recovery code button is pressed const recoveryAction = useCallback(async () => { diff --git a/src/context/intermediate/modals/Input.tsx b/src/context/intermediate/modals/Input.tsx index e34cad30..d4e20b69 100644 --- a/src/context/intermediate/modals/Input.tsx +++ b/src/context/intermediate/modals/Input.tsx @@ -6,8 +6,8 @@ import { useContext, useState } from "preact/hooks"; import { Category, InputBox, Modal } from "@revoltchat/ui"; +import { useClient } from "../../../controllers/client/ClientController"; import { I18nError } from "../../Locale"; -import { AppContext } from "../../revoltjs/RevoltClient"; import { takeError } from "../../revoltjs/util"; interface Props { @@ -89,7 +89,7 @@ type SpecialProps = { onClose: () => void } & ( export function SpecialInputModal(props: SpecialProps) { const history = useHistory(); - const client = useContext(AppContext); + const client = useClient(); const { onClose } = props; switch (props.type) { diff --git a/src/context/intermediate/modals/Prompt.tsx b/src/context/intermediate/modals/Prompt.tsx index 31fc2d59..4f9986c1 100644 --- a/src/context/intermediate/modals/Prompt.tsx +++ b/src/context/intermediate/modals/Prompt.tsx @@ -5,7 +5,7 @@ import { ulid } from "ulid"; import styles from "./Prompt.module.scss"; 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 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 UserIcon from "../../../components/common/user/UserIcon"; +import { useClient } from "../../../controllers/client/ClientController"; import { I18nError } from "../../Locale"; -import { AppContext } from "../../revoltjs/RevoltClient"; import { takeError } from "../../revoltjs/util"; import { useIntermediate } from "../Intermediate"; @@ -81,7 +81,7 @@ type SpecialProps = { onClose: () => void } & ( ); export const SpecialPromptModal = observer((props: SpecialProps) => { - const client = useContext(AppContext); + const client = useClient(); const history = useHistory(); const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); diff --git a/src/context/intermediate/popovers/CreateBot.tsx b/src/context/intermediate/popovers/CreateBot.tsx index 45837755..142996cd 100644 --- a/src/context/intermediate/popovers/CreateBot.tsx +++ b/src/context/intermediate/popovers/CreateBot.tsx @@ -2,13 +2,13 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { API } from "revolt.js"; import { Text } from "preact-i18n"; -import { useContext, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { Category, Modal } from "@revoltchat/ui"; +import { useClient } from "../../../controllers/client/ClientController"; import FormField from "../../../pages/login/FormField"; import { I18nError } from "../../Locale"; -import { AppContext } from "../../revoltjs/RevoltClient"; import { takeError } from "../../revoltjs/util"; interface Props { @@ -21,7 +21,7 @@ interface FormInputs { } export function CreateBotModal({ onClose, onCreate }: Props) { - const client = useContext(AppContext); + const client = useClient(); const { handleSubmit, register, errors } = useForm(); const [error, setError] = useState(undefined); diff --git a/src/context/intermediate/popovers/ImageViewer.tsx b/src/context/intermediate/popovers/ImageViewer.tsx index c6f8d91a..12a17f3c 100644 --- a/src/context/intermediate/popovers/ImageViewer.tsx +++ b/src/context/intermediate/popovers/ImageViewer.tsx @@ -7,7 +7,7 @@ import { Modal } from "@revoltchat/ui"; import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions"; import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions"; -import { useClient } from "../../revoltjs/RevoltClient"; +import { useClient } from "../../../controllers/client/ClientController"; interface Props { onClose: () => void; diff --git a/src/context/intermediate/popovers/UserPicker.tsx b/src/context/intermediate/popovers/UserPicker.tsx index d1aeecf8..b152e8d5 100644 --- a/src/context/intermediate/popovers/UserPicker.tsx +++ b/src/context/intermediate/popovers/UserPicker.tsx @@ -5,7 +5,7 @@ import { useState } from "preact/hooks"; import { Modal } from "@revoltchat/ui"; import UserCheckbox from "../../../components/common/user/UserCheckbox"; -import { useClient } from "../../revoltjs/RevoltClient"; +import { useClient } from "../../../controllers/client/ClientController"; interface Props { omit?: string[]; diff --git a/src/context/intermediate/popovers/UserProfile.tsx b/src/context/intermediate/popovers/UserProfile.tsx index e5244099..5a4eb4c0 100644 --- a/src/context/intermediate/popovers/UserProfile.tsx +++ b/src/context/intermediate/popovers/UserProfile.tsx @@ -34,11 +34,7 @@ import UserIcon from "../../../components/common/user/UserIcon"; import { Username } from "../../../components/common/user/UserShort"; import UserStatus from "../../../components/common/user/UserStatus"; import Markdown from "../../../components/markdown/Markdown"; -import { - ClientStatus, - StatusContext, - useClient, -} from "../../revoltjs/RevoltClient"; +import { useSession } from "../../../controllers/client/ClientController"; import { useIntermediate } from "../Intermediate"; interface Props { @@ -63,8 +59,8 @@ export const UserProfile = observer( >(); const history = useHistory(); - const client = useClient(); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; const [tab, setTab] = useState("profile"); const user = client.users.get(user_id); @@ -101,32 +97,26 @@ export const UserProfile = observer( useEffect(() => { if (dummy) return; - if ( - status === ClientStatus.ONLINE && - typeof mutual === "undefined" - ) { + if (session.state === "Online" && typeof mutual === "undefined") { setMutual(null); user.fetchMutual().then(setMutual); } - }, [mutual, status, dummy, user]); + }, [mutual, session.state, dummy, user]); useEffect(() => { if (dummy) return; - if ( - status === ClientStatus.ONLINE && - typeof profile === "undefined" - ) { + if (session.state === "Online" && typeof profile === "undefined") { setProfile(null); if (user.permission & UserPermission.ViewProfile) { user.fetchProfile().then(setProfile).catch(noop); } } - }, [profile, status, dummy, user]); + }, [profile, session.state, dummy, user]); useEffect(() => { if ( - status === ClientStatus.ONLINE && + session.state === "Online" && user.bot && typeof isPublicBot === "undefined" ) { @@ -136,7 +126,7 @@ export const UserProfile = observer( .then(() => setIsPublicBot(true)) .catch(noop); } - }, [isPublicBot, status, user, client.bots]); + }, [isPublicBot, session.state, user, client.bots]); const backgroundURL = profile && diff --git a/src/context/revoltjs/CheckAuth.tsx b/src/context/revoltjs/CheckAuth.tsx index be5c8eb8..e1dbcc55 100644 --- a/src/context/revoltjs/CheckAuth.tsx +++ b/src/context/revoltjs/CheckAuth.tsx @@ -1,8 +1,6 @@ import { Redirect } from "react-router-dom"; -import { useApplicationState } from "../../mobx/State"; - -import { useClient } from "./RevoltClient"; +import { useSession } from "../../controllers/client/ClientController"; interface Props { auth?: boolean; @@ -12,14 +10,12 @@ interface Props { } export const CheckAuth = (props: Props) => { - const auth = useApplicationState().auth; - const client = useClient(); - const ready = auth.isLoggedIn() && !!client?.user; + const session = useSession(); - if (props.auth && !ready) { + if (props.auth && !session?.ready) { if (props.blockRender) return null; return ; - } else if (!props.auth && ready) { + } else if (!props.auth && session?.ready) { if (props.blockRender) return null; return ; } diff --git a/src/context/revoltjs/FileUploads.tsx b/src/context/revoltjs/FileUploads.tsx index 5b32b51a..4ab39cbc 100644 --- a/src/context/revoltjs/FileUploads.tsx +++ b/src/context/revoltjs/FileUploads.tsx @@ -5,17 +5,15 @@ import Axios, { AxiosRequestConfig } from "axios"; import styles from "./FileUploads.module.scss"; import classNames from "classnames"; import { Text } from "preact-i18n"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { IconButton, Preloader } from "@revoltchat/ui"; 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 { AppContext } from "./RevoltClient"; import { takeError } from "./util"; type BehaviourType = @@ -115,7 +113,7 @@ export function grabFiles( export function FileUploader(props: Props) { const { fileType, maxFileSize, remove } = props; const { openScreen } = useIntermediate(); - const client = useApplicationState().client!; + const client = useClient(); const [uploading, setUploading] = useState(false); diff --git a/src/context/revoltjs/Notifications.tsx b/src/context/revoltjs/Notifications.tsx index 48178c63..c1d144a1 100644 --- a/src/context/revoltjs/Notifications.tsx +++ b/src/context/revoltjs/Notifications.tsx @@ -2,13 +2,13 @@ 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 { useCallback, useEffect } from "preact/hooks"; import { useTranslation } from "../../lib/i18n"; import { useApplicationState } from "../../mobx/State"; -import { AppContext } from "./RevoltClient"; +import { useClient } from "../../controllers/client/ClientController"; const notifications: { [key: string]: Notification } = {}; @@ -30,7 +30,7 @@ function Notifier() { const notifs = state.notifications; const showNotification = state.settings.get("notifications:desktop"); - const client = useContext(AppContext); + const client = useClient(); const { guild: guild_id, channel: channel_id } = useParams<{ guild: string; channel: string; diff --git a/src/context/revoltjs/RequiresOnline.tsx b/src/context/revoltjs/RequiresOnline.tsx index 4835bd42..1cc65038 100644 --- a/src/context/revoltjs/RequiresOnline.tsx +++ b/src/context/revoltjs/RequiresOnline.tsx @@ -2,11 +2,10 @@ import { WifiOff } from "@styled-icons/boxicons-regular"; import styled from "styled-components/macro"; import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; import { Preloader } from "@revoltchat/ui"; -import { ClientStatus, StatusContext } from "./RevoltClient"; +import { useSession } from "../../controllers/client/ClientController"; interface Props { children: Children; @@ -29,10 +28,12 @@ const Base = styled.div` `; export default function RequiresOnline(props: Props) { - const status = useContext(StatusContext); + const session = useSession(); - if (status === ClientStatus.CONNECTING) return ; - if (status !== ClientStatus.ONLINE && status !== ClientStatus.READY) + if (!session || session.state === "Connecting") + return ; + + if (!(session.state === "Online" || session.state === "Ready")) return ( diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx index b76f4585..145a2550 100644 --- a/src/context/revoltjs/RevoltClient.tsx +++ b/src/context/revoltjs/RevoltClient.tsx @@ -1,113 +1,27 @@ /* 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 { useEffect } from "preact/hooks"; import { Preloader } from "@revoltchat/ui"; import { useApplicationState } from "../../mobx/State"; import { clientController } from "../../controllers/client/ClientController"; -import { modalController } from "../../controllers/modals/ModalController"; -import { registerEvents } from "./events"; -import { takeError } from "./util"; - -export enum ClientStatus { - READY, - LOADING, - OFFLINE, - DISCONNECTED, - CONNECTING, - RECONNECTING, - ONLINE, -} - -export interface ClientOperations { - logout: (shouldRequest?: boolean) => Promise; -} - -export const AppContext = createContext(null!); -export const StatusContext = createContext(null!); -export const LogOutContext = createContext<(avoidReq?: boolean) => void>(null!); type Props = { children: Children; }; export default observer(({ children }: Props) => { - // const state = useApplicationState(); - /*const [client, setClient] = useState(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]); - - if (!loaded || status === ClientStatus.LOADING) { - return ; - }*/ - const session = clientController.getActiveSession(); - if (!session?.ready) { - return ; + if (session) { + if (!session.ready) return ; + + const client = session.client!; + const state = useApplicationState(); + useEffect(() => state.registerListeners(client), [state, client]); } - const client = session.client!; - const state = useApplicationState(); - useEffect(() => state.registerListeners(client), [state, client]); - - return ( - - - void {}}> - {children} - - - - ); + return <>{children}; }); - -export const useClient = () => useContext(AppContext); diff --git a/src/context/revoltjs/StateMonitor.tsx b/src/context/revoltjs/StateMonitor.tsx deleted file mode 100644 index 34400f0b..00000000 --- a/src/context/revoltjs/StateMonitor.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * This file monitors the message cache to delete any queued messages that have already sent. - */ -import { Message } from "revolt.js"; - -import { useContext, useEffect } from "preact/hooks"; - -import { useApplicationState } from "../../mobx/State"; - -import { setGlobalEmojiPack } from "../../components/common/Emoji"; - -import { AppContext } from "./RevoltClient"; - -export default function StateMonitor() { - const client = useContext(AppContext); - const state = useApplicationState(); - - useEffect(() => { - function add(msg: Message) { - if (!msg.nonce) return; - if ( - !state.queue.get(msg.channel_id).find((x) => x.id === msg.nonce) - ) - return; - state.queue.remove(msg.nonce); - } - - client.addListener("message", add); - return () => client.removeListener("message", add); - }, [client]); - - // Set global emoji pack. - useEffect(() => { - const v = state.settings.get("appearance:emoji"); - v && setGlobalEmojiPack(v); - }, [state.settings.get("appearance:emoji")]); - - return null; -} diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx index 6ed39af4..5d72f8c2 100644 --- a/src/context/revoltjs/SyncManager.tsx +++ b/src/context/revoltjs/SyncManager.tsx @@ -9,7 +9,7 @@ import { reportError } from "../../lib/ErrorBoundary"; import { useApplicationState } from "../../mobx/State"; -import { useClient } from "./RevoltClient"; +import { useClient } from "../../controllers/client/ClientController"; export default function SyncManager() { const client = useClient(); diff --git a/src/context/revoltjs/events.ts b/src/context/revoltjs/events.ts index a4121bc9..8232036f 100644 --- a/src/context/revoltjs/events.ts +++ b/src/context/revoltjs/events.ts @@ -1,15 +1,6 @@ -import { Client, Server } from "revolt.js"; +export const _ = ""; -import { StateUpdater } from "preact/hooks"; - -import { deleteRenderer } from "../../lib/renderer/Singleton"; - -import State from "../../mobx/State"; - -import { resetMemberSidebarFetched } from "../../components/navigation/right/MemberSidebar"; -import { ClientStatus } from "./RevoltClient"; - -export function registerEvents( +/*export function registerEvents( state: State, setStatus: StateUpdater, client: Client, @@ -86,4 +77,4 @@ export function registerEvents( window.removeEventListener("online", online); window.removeEventListener("offline", offline); }; -} +}*/ diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index 56200d97..df3216bd 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -1,11 +1,17 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; -import type { Nullable } from "revolt.js"; +import { Client, Nullable } from "revolt.js"; import Auth from "../../mobx/stores/Auth"; +import { modalController } from "../modals/ModalController"; import Session from "./Session"; class ClientController { + /** + * API client + */ + private apiClient: Client; + /** * Map of user IDs to sessions */ @@ -17,10 +23,19 @@ class ClientController { private current: Nullable; constructor() { + this.apiClient = new Client({ + apiURL: import.meta.env.VITE_API_URL, + }); + this.sessions = new ObservableMap(); this.current = null; makeAutoObservable(this); + + this.logoutCurrent = this.logoutCurrent.bind(this); + + // Inject globally + (window as any).clientController = this; } /** @@ -29,12 +44,23 @@ class ClientController { */ @action hydrate(auth: Auth) { for (const entry of auth.getAccounts()) { + const user_id = entry.session.user_id!; + const session = new Session(); - this.sessions.set(entry.session._id!, session); - session.emit({ - action: "LOGIN", - session: entry.session, - }); + this.sessions.set(user_id, session); + + session + .emit({ + action: "LOGIN", + session: entry.session, + }) + .catch((error) => { + if (error === "Forbidden" || error === "Unauthorized") { + this.sessions.delete(user_id); + auth.removeSession(user_id); + modalController.push({ type: "signed_out" }); + } + }); } this.current = this.sessions.keys().next().value ?? null; @@ -44,6 +70,14 @@ class ClientController { return this.sessions.get(this.current!); } + @computed getAnonymousClient() { + return this.apiClient; + } + + @computed getAvailableClient() { + return this.getActiveSession()?.client ?? this.apiClient; + } + @computed isLoggedIn() { return this.current === null; } @@ -59,6 +93,41 @@ class ClientController { session.destroy(); } } + + @action logoutCurrent() { + if (this.current) { + this.logout(this.current); + } + } + + @action switchAccount(user_id: string) { + this.current = user_id; + } } export const clientController = new ClientController(); + +/** + * Get the currently active session. + * @returns Session + */ +export function useSession() { + return clientController.getActiveSession(); +} + +/** + * Get the currently active client or an unauthorised + * client for API requests, whichever is available. + * @returns Revolt.js Client + */ +export function useClient() { + return clientController.getAvailableClient(); +} + +/** + * Get unauthorised client for API requests. + * @returns Revolt.js Client + */ +export function useApi() { + return clientController.getAnonymousClient().api; +} diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index 1189e059..b19750e5 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -20,6 +20,7 @@ type Transition = export default class Session { state: State = window.navigator.onLine ? "Ready" : "Offline"; + user_id: string | null = null; client: Client | null = null; constructor() { @@ -83,6 +84,7 @@ export default class Session { private destroyClient() { this.client!.removeAllListeners(); + this.user_id = null; this.client = null; } @@ -101,7 +103,7 @@ export default class Session { } @action async emit(data: Transition) { - console.info("Handle event:", data); + console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data); switch (data.action) { // Login with session @@ -112,6 +114,7 @@ export default class Session { try { await this.client!.useExistingSession(data.session); + this.user_id = this.client!.user!._id; } catch (err) { this.state = "Ready"; throw err; diff --git a/src/controllers/modals/components/ModifyAccount.tsx b/src/controllers/modals/components/ModifyAccount.tsx index 2971e48e..70dd3d11 100644 --- a/src/controllers/modals/components/ModifyAccount.tsx +++ b/src/controllers/modals/components/ModifyAccount.tsx @@ -1,18 +1,16 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { Text } from "preact-i18n"; -import { useContext, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { Category, Error, Modal } from "@revoltchat/ui"; import { noopTrue } from "../../../lib/js"; -import { useApplicationState } from "../../../mobx/State"; - -import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { takeError } from "../../../context/revoltjs/util"; import FormField from "../../../pages/login/FormField"; +import { useClient } from "../../client/ClientController"; import { ModalProps } from "../types"; interface FormInputs { @@ -30,7 +28,7 @@ export default function ModifyAccount({ field, ...props }: ModalProps<"modify_account">) { - const client = useApplicationState().client!; + const client = useClient(); const [processing, setProcessing] = useState(false); const { handleSubmit, register, errors } = useForm(); const [error, setError] = useState(undefined); diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 2dc652d7..ed96762c 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -19,7 +19,6 @@ import { openContextMenu, } from "preact-context-menu"; import { Text } from "preact-i18n"; -import { useContext } from "preact/hooks"; import { IconButton, LineDivider } from "@revoltchat/ui"; @@ -28,16 +27,12 @@ import { QueuedMessage } from "../mobx/stores/MessageQueue"; import { NotificationState } from "../mobx/stores/NotificationOptions"; import { Screen, useIntermediate } from "../context/intermediate/Intermediate"; -import { - AppContext, - ClientStatus, - StatusContext, -} from "../context/revoltjs/RevoltClient"; import { takeError } from "../context/revoltjs/util"; import CMNotifications from "./contextmenu/CMNotifications"; import Tooltip from "../components/common/Tooltip"; import UserStatus from "../components/common/user/UserStatus"; +import { useSession } from "../controllers/client/ClientController"; import { modalController } from "../controllers/modals/ModalController"; import { internalEmit } from "./eventEmitter"; import { getRenderer } from "./renderer/Singleton"; @@ -122,12 +117,12 @@ type Action = // Tip: This should just be split into separate context menus per logical area. export default function ContextMenus() { const { openScreen, writeClipboard } = useIntermediate(); - const client = useContext(AppContext); + const session = useSession()!; + const client = session.client!; const userId = client.user!._id; - const status = useContext(StatusContext); - const isOnline = status === ClientStatus.ONLINE; const state = useApplicationState(); const history = useHistory(); + const isOnline = session.state === "Online"; function contextClick(data?: Action) { if (typeof data === "undefined") return; diff --git a/src/lib/FakeClient.tsx b/src/lib/FakeClient.tsx deleted file mode 100644 index 861ecec3..00000000 --- a/src/lib/FakeClient.tsx +++ /dev/null @@ -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 {children}; -}); diff --git a/src/mobx/State.ts b/src/mobx/State.ts index 97299b69..7a951ab9 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -140,6 +140,9 @@ export default class State { if (client) { this.client = client; this.plugins.onClient(client); + + // Register message listener for clearing queue. + this.client.addListener("message", this.queue.onMessage); } // Register all the listeners required for saving and syncing state. @@ -225,6 +228,11 @@ export default class State { }); return () => { + // Remove any listeners attached to client. + if (client) { + client.removeListener("message", this.queue.onMessage); + } + // Stop exposing the client. this.client = undefined; diff --git a/src/mobx/stores/MessageQueue.ts b/src/mobx/stores/MessageQueue.ts index 953427ed..c4fd5d26 100644 --- a/src/mobx/stores/MessageQueue.ts +++ b/src/mobx/stores/MessageQueue.ts @@ -5,6 +5,7 @@ import { makeAutoObservable, observable, } from "mobx"; +import { Message } from "revolt.js"; import Store from "../interfaces/Store"; @@ -47,6 +48,8 @@ export default class MessageQueue implements Store { constructor() { this.messages = observable.array([]); makeAutoObservable(this); + + this.onMessage = this.onMessage.bind(this); } get id() { @@ -105,4 +108,16 @@ export default class MessageQueue implements Store { @computed get(channel: string) { return this.messages.filter((x) => x.channel === channel); } + + /** + * Handle an incoming Message + * @param message Message + */ + @action onMessage(message: Message) { + if (!message.nonce) return; + if (!this.get(message.channel_id).find((x) => x.id === message.nonce)) + return; + + this.remove(message.nonce); + } } diff --git a/src/mobx/stores/Settings.ts b/src/mobx/stores/Settings.ts index f775a366..344d65a7 100644 --- a/src/mobx/stores/Settings.ts +++ b/src/mobx/stores/Settings.ts @@ -4,8 +4,7 @@ import { mapToRecord } from "../../lib/conversion"; import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme"; -import { EmojiPack } from "../../components/common/Emoji"; -import { MIGRATIONS } from "../State"; +import { EmojiPack, setGlobalEmojiPack } from "../../components/common/Emoji"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; import Syncable from "../interfaces/Syncable"; @@ -79,6 +78,11 @@ export default class Settings * @param value Value */ @action set(key: T, value: ISettings[T]) { + // Emoji needs to be immediately applied. + if (key === 'appearance:emoji') { + setGlobalEmojiPack(value as EmojiPack); + } + this.data.set(key, value); } diff --git a/src/pages/Open.tsx b/src/pages/Open.tsx index 40dd5f7a..cf724c84 100644 --- a/src/pages/Open.tsx +++ b/src/pages/Open.tsx @@ -2,25 +2,20 @@ import { useHistory, useParams } from "react-router-dom"; import { Text } from "preact-i18n"; -import { useContext, useEffect } from "preact/hooks"; +import { useEffect } from "preact/hooks"; import { Header } from "@revoltchat/ui"; -import { - AppContext, - ClientStatus, - StatusContext, -} from "../context/revoltjs/RevoltClient"; - +import { useSession } from "../controllers/client/ClientController"; import { modalController } from "../controllers/modals/ModalController"; export default function Open() { const history = useHistory(); - const client = useContext(AppContext); - const status = useContext(StatusContext); + const session = useSession()!; + const client = session.client!; const { id } = useParams<{ id: string }>(); - if (status !== ClientStatus.ONLINE) { + if (session.state !== "Online") { return (
diff --git a/src/pages/RevoltApp.tsx b/src/pages/RevoltApp.tsx index c4b3631b..110b9b3a 100644 --- a/src/pages/RevoltApp.tsx +++ b/src/pages/RevoltApp.tsx @@ -9,7 +9,6 @@ import { isTouchscreenDevice } from "../lib/isTouchscreenDevice"; import Popovers from "../context/intermediate/Popovers"; import Notifications from "../context/revoltjs/Notifications"; -import StateMonitor from "../context/revoltjs/StateMonitor"; import { Titlebar } from "../components/native/Titlebar"; import BottomNavigation from "../components/navigation/BottomNavigation"; @@ -235,7 +234,6 @@ export default function App() { - diff --git a/src/pages/app.tsx b/src/pages/app.tsx index 89f05b31..e50a27c8 100644 --- a/src/pages/app.tsx +++ b/src/pages/app.tsx @@ -5,7 +5,6 @@ import { lazy, Suspense } from "preact/compat"; import { Masks, Preloader } from "@revoltchat/ui"; import ErrorBoundary from "../lib/ErrorBoundary"; -import FakeClient from "../lib/FakeClient"; import Context from "../context"; import { CheckAuth } from "../context/revoltjs/CheckAuth"; @@ -36,9 +35,7 @@ export function App() { - - - + diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx index 49d4028f..3545fd55 100644 --- a/src/pages/channels/Channel.tsx +++ b/src/pages/channels/Channel.tsx @@ -16,8 +16,6 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout"; -import { useClient } from "../../context/revoltjs/RevoltClient"; - import AgeGate from "../../components/common/AgeGate"; import MessageBox from "../../components/common/messaging/MessageBox"; import JumpToBottom from "../../components/common/messaging/bars/JumpToBottom"; @@ -25,6 +23,7 @@ import NewMessages from "../../components/common/messaging/bars/NewMessages"; import TypingIndicator from "../../components/common/messaging/bars/TypingIndicator"; import RightSidebar from "../../components/navigation/RightSidebar"; import { PageHeader } from "../../components/ui/Header"; +import { useClient } from "../../controllers/client/ClientController"; import ChannelHeader from "./ChannelHeader"; import { MessageArea } from "./messaging/MessageArea"; import VoiceHeader from "./voice/VoiceHeader"; diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index 4a1e031e..b74a9ad9 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -25,11 +25,8 @@ import { ScrollState } from "../../../lib/renderer/types"; import { IntermediateContext } from "../../../context/intermediate/Intermediate"; import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; -import { - ClientStatus, - StatusContext, -} from "../../../context/revoltjs/RevoltClient"; +import { useSession } from "../../../controllers/client/ClientController"; import ConversationStart from "./ConversationStart"; import MessageRenderer from "./MessageRenderer"; @@ -65,7 +62,7 @@ export const MESSAGE_AREA_PADDING = 82; export const MessageArea = observer(({ last_id, channel }: Props) => { const history = useHistory(); - const status = useContext(StatusContext); + const session = useSession()!; const { focusTaken } = useContext(IntermediateContext); // ? 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. useEffect(() => { - switch (status) { - case ClientStatus.ONLINE: + switch (session.state) { + case "Online": if (renderer.state === "WAITING_FOR_NETWORK") { renderer.init(); } else { @@ -222,13 +219,13 @@ export const MessageArea = observer(({ last_id, channel }: Props) => { } break; - case ClientStatus.OFFLINE: - case ClientStatus.DISCONNECTED: - case ClientStatus.CONNECTING: + case "Offline": + case "Disconnected": + case "Connecting": renderer.markStale(); break; } - }, [renderer, status]); + }, [renderer, session.state]); // ? When the container is scrolled. // ? Also handle StayAtBottom diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index fd7aa7b9..fa90cf0e 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -3,9 +3,7 @@ import { X } from "@styled-icons/boxicons-regular"; import dayjs from "dayjs"; import isEqual from "lodash.isequal"; import { observer } from "mobx-react-lite"; -import { API } from "revolt.js"; -import { Message as MessageI } from "revolt.js"; -import { Nullable } from "revolt.js"; +import { API, Message as MessageI, Nullable } from "revolt.js"; import styled from "styled-components/macro"; import { decodeTime } from "ulid"; @@ -20,10 +18,10 @@ import { ChannelRenderer } from "../../../lib/renderer/Singleton"; import { useApplicationState } from "../../../mobx/State"; import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; import Message from "../../../components/common/messaging/Message"; import { SystemMessage } from "../../../components/common/messaging/SystemMessage"; +import { useClient } from "../../../controllers/client/ClientController"; import ConversationStart from "./ConversationStart"; import MessageEditor from "./MessageEditor"; diff --git a/src/pages/channels/voice/VoiceHeader.tsx b/src/pages/channels/voice/VoiceHeader.tsx index 0d906868..242e1041 100644 --- a/src/pages/channels/voice/VoiceHeader.tsx +++ b/src/pages/channels/voice/VoiceHeader.tsx @@ -17,10 +17,10 @@ import { Button } from "@revoltchat/ui"; import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../context/revoltjs/RevoltClient"; import Tooltip from "../../../components/common/Tooltip"; import UserIcon from "../../../components/common/user/UserIcon"; +import { useClient } from "../../../controllers/client/ClientController"; interface Props { id: string; diff --git a/src/pages/developer/Developer.tsx b/src/pages/developer/Developer.tsx index 46733c44..c9e7f617 100644 --- a/src/pages/developer/Developer.tsx +++ b/src/pages/developer/Developer.tsx @@ -1,18 +1,17 @@ import { Wrench } from "@styled-icons/boxicons-solid"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import PaintCounter from "../../lib/PaintCounter"; import { TextReact } from "../../lib/i18n"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; - import { PageHeader } from "../../components/ui/Header"; +import { useClient } from "../../controllers/client/ClientController"; export default function Developer() { // const voice = useContext(VoiceContext); - const client = useContext(AppContext); + const client = useClient(); const userPermission = client.user!.permission; const [ping, setPing] = useState(client.websocket.ping); const [crash, setCrash] = useState(false); diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx index e269755f..3b6fa516 100644 --- a/src/pages/friends/Friends.tsx +++ b/src/pages/friends/Friends.tsx @@ -13,12 +13,12 @@ import { TextReact } from "../../lib/i18n"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { useClient } from "../../context/revoltjs/RevoltClient"; import CollapsibleSection from "../../components/common/CollapsibleSection"; import Tooltip from "../../components/common/Tooltip"; import UserIcon from "../../components/common/user/UserIcon"; import { PageHeader } from "../../components/ui/Header"; +import { useClient } from "../../controllers/client/ClientController"; import { modalController } from "../../controllers/modals/ModalController"; import { Friend } from "./Friend"; diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index 858161c8..9690af9d 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -15,7 +15,7 @@ import styled from "styled-components/macro"; import styles from "./Home.module.scss"; import "./snow.scss"; import { Text } from "preact-i18n"; -import { useContext, useMemo } from "preact/hooks"; +import { useMemo } from "preact/hooks"; import { CategoryButton } from "@revoltchat/ui"; @@ -24,11 +24,11 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { AppContext } from "../../context/revoltjs/RevoltClient"; import wideSVG from "/assets/wide.svg"; import { PageHeader } from "../../components/ui/Header"; +import { useClient } from "../../controllers/client/ClientController"; const Overlay = styled.div` display: grid; @@ -45,7 +45,7 @@ const Overlay = styled.div` export default observer(() => { const { openScreen } = useIntermediate(); - const client = useContext(AppContext); + const client = useClient(); const state = useApplicationState(); const seasonalTheme = state.settings.get("appearance:seasonal", true); diff --git a/src/pages/invite/Invite.tsx b/src/pages/invite/Invite.tsx index b11f0d96..1b28a9f0 100644 --- a/src/pages/invite/Invite.tsx +++ b/src/pages/invite/Invite.tsx @@ -1,11 +1,10 @@ import { ArrowBack } from "@styled-icons/boxicons-regular"; -import { autorun } from "mobx"; import { Redirect, useHistory, useParams } from "react-router-dom"; import { API } from "revolt.js"; import styles from "./Invite.module.scss"; import { Text } from "preact-i18n"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { Button, Category, Error, Preloader } from "@revoltchat/ui"; @@ -14,23 +13,22 @@ import { TextReact } from "../../lib/i18n"; import { useApplicationState } from "../../mobx/State"; import RequiresOnline from "../../context/revoltjs/RequiresOnline"; -import { - AppContext, - ClientStatus, - StatusContext, -} from "../../context/revoltjs/RevoltClient"; import { takeError } from "../../context/revoltjs/util"; import ServerIcon from "../../components/common/ServerIcon"; import UserIcon from "../../components/common/user/UserIcon"; +import { + useClient, + useSession, +} from "../../controllers/client/ClientController"; export default function Invite() { const history = useHistory(); - const client = useContext(AppContext); + const session = useSession(); + const client = useClient(); const layout = useApplicationState().layout; - const status = useContext(StatusContext); const { code } = useParams<{ code: string }>(); const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); @@ -45,7 +43,7 @@ export default function Invite() { .then((data) => setInvite(data)) .catch((err) => setError(takeError(err))); } - }, [client, code, invite, status]); + }, [code, invite]); if (code === undefined) return ; @@ -154,7 +152,7 @@ export default function Invite() {