From 1cfcb20d4de2ea643d90f2ed33aa1238c75407c2 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Mon, 27 Jun 2022 17:56:06 +0100 Subject: [PATCH 01/17] chore(refactor): rename `context/modals` to `controllers/modals` --- external/lang | 2 +- src/components/common/messaging/MessageBox.tsx | 2 +- src/components/settings/account/AccountManagement.tsx | 3 ++- src/components/settings/account/EditAccount.tsx | 3 ++- .../settings/account/MultiFactorAuthentication.tsx | 3 ++- src/context/DO_NOT_TOUCH.md | 2 ++ src/context/index.tsx | 2 +- src/context/intermediate/Intermediate.tsx | 2 +- src/context/revoltjs/FileUploads.tsx | 2 +- src/context/revoltjs/RevoltClient.tsx | 2 +- .../index.tsx => controllers/modals/ModalController.tsx} | 7 ++++--- src/{context => controllers}/modals/ModalRenderer.tsx | 2 +- .../modals/components/Changelog.tsx | 0 .../modals/components/Clipboard.tsx | 0 src/{context => controllers}/modals/components/Error.tsx | 0 .../modals/components/LinkWarning.tsx | 0 .../modals/components/MFAEnableTOTP.tsx | 0 src/{context => controllers}/modals/components/MFAFlow.tsx | 0 .../modals/components/MFARecovery.tsx | 2 +- .../modals/components/ModifyAccount.tsx | 5 +++-- .../modals/components/OutOfDate.tsx | 0 .../modals/components/PendingFriendRequests.tsx | 0 .../modals/components/ServerIdentity.tsx | 3 ++- .../modals/components/ShowToken.tsx | 0 .../modals/components/SignOutSessions.tsx | 0 .../modals/components/SignedOut.tsx | 0 src/{context => controllers}/modals/types.ts | 0 src/lib/ContextMenus.tsx | 2 +- src/mobx/stores/Changelog.ts | 3 +-- src/pages/Open.tsx | 3 ++- src/pages/friends/Friends.tsx | 2 +- src/pages/login/forms/FormLogin.tsx | 2 +- src/pages/settings/GenericSettings.tsx | 3 +-- src/pages/settings/Settings.tsx | 2 +- src/pages/settings/panes/MyBots.tsx | 2 +- src/pages/settings/panes/Notifications.tsx | 3 ++- src/pages/settings/panes/Sessions.tsx | 3 ++- src/updateWorker.ts | 3 +-- 38 files changed, 39 insertions(+), 31 deletions(-) create mode 100644 src/context/DO_NOT_TOUCH.md rename src/{context/modals/index.tsx => controllers/modals/ModalController.tsx} (97%) rename src/{context => controllers}/modals/ModalRenderer.tsx (91%) rename src/{context => controllers}/modals/components/Changelog.tsx (100%) rename src/{context => controllers}/modals/components/Clipboard.tsx (100%) rename src/{context => controllers}/modals/components/Error.tsx (100%) rename src/{context => controllers}/modals/components/LinkWarning.tsx (100%) rename src/{context => controllers}/modals/components/MFAEnableTOTP.tsx (100%) rename src/{context => controllers}/modals/components/MFAFlow.tsx (100%) rename src/{context => controllers}/modals/components/MFARecovery.tsx (97%) rename src/{context => controllers}/modals/components/ModifyAccount.tsx (97%) rename src/{context => controllers}/modals/components/OutOfDate.tsx (100%) rename src/{context => controllers}/modals/components/PendingFriendRequests.tsx (100%) rename src/{context => controllers}/modals/components/ServerIdentity.tsx (98%) rename src/{context => controllers}/modals/components/ShowToken.tsx (100%) rename src/{context => controllers}/modals/components/SignOutSessions.tsx (100%) rename src/{context => controllers}/modals/components/SignedOut.tsx (100%) rename src/{context => controllers}/modals/types.ts (100%) diff --git a/external/lang b/external/lang index 50838167..def08f21 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit 50838167d7d253de9d08715e6a6070c3ddc9fcc2 +Subproject commit def08f210e9edc4f203cb38611fd270761102860 diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 0c7acc9d..4bb4dd49 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -24,7 +24,6 @@ import { import { useApplicationState } from "../../../mobx/State"; import { Reply } from "../../../mobx/stores/MessageQueue"; -import { modalController } from "../../../context/modals"; import { FileUploader, grabFiles, @@ -33,6 +32,7 @@ import { import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { takeError } from "../../../context/revoltjs/util"; +import { modalController } from "../../../controllers/modals/ModalController"; import AutoComplete, { useAutoComplete } from "../AutoComplete"; import { PermissionTooltip } from "../Tooltip"; import FilePreview from "./bars/FilePreview"; diff --git a/src/components/settings/account/AccountManagement.tsx b/src/components/settings/account/AccountManagement.tsx index 7af4b367..f7ac5843 100644 --- a/src/components/settings/account/AccountManagement.tsx +++ b/src/components/settings/account/AccountManagement.tsx @@ -6,12 +6,13 @@ import { useContext } from "preact/hooks"; import { CategoryButton } from "@revoltchat/ui"; -import { modalController } from "../../../context/modals"; import { LogOutContext, useClient, } from "../../../context/revoltjs/RevoltClient"; +import { modalController } from "../../../controllers/modals/ModalController"; + export default function AccountManagement() { const logOut = useContext(LogOutContext); const client = useClient(); diff --git a/src/components/settings/account/EditAccount.tsx b/src/components/settings/account/EditAccount.tsx index d2ccdeac..929bdfb1 100644 --- a/src/components/settings/account/EditAccount.tsx +++ b/src/components/settings/account/EditAccount.tsx @@ -12,13 +12,14 @@ import { HiddenValue, } from "@revoltchat/ui"; -import { modalController } from "../../../context/modals"; import { ClientStatus, StatusContext, useClient, } from "../../../context/revoltjs/RevoltClient"; +import { modalController } from "../../../controllers/modals/ModalController"; + export default observer(() => { const client = useClient(); const status = useContext(StatusContext); diff --git a/src/components/settings/account/MultiFactorAuthentication.tsx b/src/components/settings/account/MultiFactorAuthentication.tsx index 1b7bfbf8..d1cca41d 100644 --- a/src/components/settings/account/MultiFactorAuthentication.tsx +++ b/src/components/settings/account/MultiFactorAuthentication.tsx @@ -7,7 +7,6 @@ import { useCallback, useContext, useEffect, useState } from "preact/hooks"; import { Category, CategoryButton, Error, Tip } from "@revoltchat/ui"; -import { modalController } from "../../../context/modals"; import { ClientStatus, StatusContext, @@ -15,6 +14,8 @@ import { } from "../../../context/revoltjs/RevoltClient"; import { takeError } from "../../../context/revoltjs/util"; +import { modalController } from "../../../controllers/modals/ModalController"; + /** * Temporary helper function for Axios config * @param token Token diff --git a/src/context/DO_NOT_TOUCH.md b/src/context/DO_NOT_TOUCH.md new file mode 100644 index 00000000..db75657b --- /dev/null +++ b/src/context/DO_NOT_TOUCH.md @@ -0,0 +1,2 @@ +hello do not touch `intermediate` or `revoltjs` folders +they are being rewritten diff --git a/src/context/index.tsx b/src/context/index.tsx index 663fdfaa..1785174b 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -8,11 +8,11 @@ import { Preloader, UIProvider } from "@revoltchat/ui"; import { hydrateState } from "../mobx/State"; +import ModalRenderer from "../controllers/modals/ModalRenderer"; import Locale from "./Locale"; import Theme from "./Theme"; import { history } from "./history"; import Intermediate from "./intermediate/Intermediate"; -import ModalRenderer from "./modals/ModalRenderer"; import Client from "./revoltjs/RevoltClient"; import SyncManager from "./revoltjs/SyncManager"; diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 3b20ff0c..32df0c1b 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -18,7 +18,7 @@ import { determineLink } from "../../lib/links"; import { useApplicationState } from "../../mobx/State"; -import { modalController } from "../modals"; +import { modalController } from "../../controllers/modals/ModalController"; import Modals from "./Modals"; export type Screen = diff --git a/src/context/revoltjs/FileUploads.tsx b/src/context/revoltjs/FileUploads.tsx index 63c24928..5b32b51a 100644 --- a/src/context/revoltjs/FileUploads.tsx +++ b/src/context/revoltjs/FileUploads.tsx @@ -13,8 +13,8 @@ import { determineFileSize } from "../../lib/fileSize"; import { useApplicationState } from "../../mobx/State"; +import { modalController } from "../../controllers/modals/ModalController"; import { useIntermediate } from "../intermediate/Intermediate"; -import { modalController } from "../modals"; import { AppContext } from "./RevoltClient"; import { takeError } from "./util"; diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx index 733b1c9e..f16f3ac1 100644 --- a/src/context/revoltjs/RevoltClient.tsx +++ b/src/context/revoltjs/RevoltClient.tsx @@ -9,7 +9,7 @@ import { Preloader } from "@revoltchat/ui"; import { useApplicationState } from "../../mobx/State"; -import { modalController } from "../modals"; +import { modalController } from "../../controllers/modals/ModalController"; import { registerEvents } from "./events"; import { takeError } from "./util"; diff --git a/src/context/modals/index.tsx b/src/controllers/modals/ModalController.tsx similarity index 97% rename from src/context/modals/index.tsx rename to src/controllers/modals/ModalController.tsx index c2c062d0..48bb810d 100644 --- a/src/context/modals/index.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -10,10 +10,11 @@ import { ulid } from "ulid"; import { determineLink } from "../../lib/links"; -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 Clipboard from "./components/Clipboard"; diff --git a/src/context/modals/ModalRenderer.tsx b/src/controllers/modals/ModalRenderer.tsx similarity index 91% rename from src/context/modals/ModalRenderer.tsx rename to src/controllers/modals/ModalRenderer.tsx index 0092d93b..712a4dc4 100644 --- a/src/context/modals/ModalRenderer.tsx +++ b/src/controllers/modals/ModalRenderer.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite"; import { useEffect } from "preact/hooks"; -import { modalController } from "."; +import { modalController } from "./ModalController"; export default observer(() => { useEffect(() => { diff --git a/src/context/modals/components/Changelog.tsx b/src/controllers/modals/components/Changelog.tsx similarity index 100% rename from src/context/modals/components/Changelog.tsx rename to src/controllers/modals/components/Changelog.tsx diff --git a/src/context/modals/components/Clipboard.tsx b/src/controllers/modals/components/Clipboard.tsx similarity index 100% rename from src/context/modals/components/Clipboard.tsx rename to src/controllers/modals/components/Clipboard.tsx diff --git a/src/context/modals/components/Error.tsx b/src/controllers/modals/components/Error.tsx similarity index 100% rename from src/context/modals/components/Error.tsx rename to src/controllers/modals/components/Error.tsx diff --git a/src/context/modals/components/LinkWarning.tsx b/src/controllers/modals/components/LinkWarning.tsx similarity index 100% rename from src/context/modals/components/LinkWarning.tsx rename to src/controllers/modals/components/LinkWarning.tsx diff --git a/src/context/modals/components/MFAEnableTOTP.tsx b/src/controllers/modals/components/MFAEnableTOTP.tsx similarity index 100% rename from src/context/modals/components/MFAEnableTOTP.tsx rename to src/controllers/modals/components/MFAEnableTOTP.tsx diff --git a/src/context/modals/components/MFAFlow.tsx b/src/controllers/modals/components/MFAFlow.tsx similarity index 100% rename from src/context/modals/components/MFAFlow.tsx rename to src/controllers/modals/components/MFAFlow.tsx diff --git a/src/context/modals/components/MFARecovery.tsx b/src/controllers/modals/components/MFARecovery.tsx similarity index 97% rename from src/context/modals/components/MFARecovery.tsx rename to src/controllers/modals/components/MFARecovery.tsx index bed9afd1..33a11809 100644 --- a/src/context/modals/components/MFARecovery.tsx +++ b/src/controllers/modals/components/MFARecovery.tsx @@ -7,8 +7,8 @@ import { Modal } from "@revoltchat/ui"; import { noopTrue } from "../../../lib/js"; -import { modalController } from ".."; import { toConfig } from "../../../components/settings/account/MultiFactorAuthentication"; +import { modalController } from "../ModalController"; import { ModalProps } from "../types"; /** diff --git a/src/context/modals/components/ModifyAccount.tsx b/src/controllers/modals/components/ModifyAccount.tsx similarity index 97% rename from src/context/modals/components/ModifyAccount.tsx rename to src/controllers/modals/components/ModifyAccount.tsx index 282c1e13..2971e48e 100644 --- a/src/context/modals/components/ModifyAccount.tsx +++ b/src/controllers/modals/components/ModifyAccount.tsx @@ -9,9 +9,10 @@ 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 { AppContext } from "../../revoltjs/RevoltClient"; -import { takeError } from "../../revoltjs/util"; import { ModalProps } from "../types"; interface FormInputs { diff --git a/src/context/modals/components/OutOfDate.tsx b/src/controllers/modals/components/OutOfDate.tsx similarity index 100% rename from src/context/modals/components/OutOfDate.tsx rename to src/controllers/modals/components/OutOfDate.tsx diff --git a/src/context/modals/components/PendingFriendRequests.tsx b/src/controllers/modals/components/PendingFriendRequests.tsx similarity index 100% rename from src/context/modals/components/PendingFriendRequests.tsx rename to src/controllers/modals/components/PendingFriendRequests.tsx diff --git a/src/context/modals/components/ServerIdentity.tsx b/src/controllers/modals/components/ServerIdentity.tsx similarity index 98% rename from src/context/modals/components/ServerIdentity.tsx rename to src/controllers/modals/components/ServerIdentity.tsx index 50c923ce..5a8dfc9a 100644 --- a/src/context/modals/components/ServerIdentity.tsx +++ b/src/controllers/modals/components/ServerIdentity.tsx @@ -19,7 +19,8 @@ import { import { noop } from "../../../lib/js"; -import { FileUploader } from "../../revoltjs/FileUploads"; +import { FileUploader } from "../../../context/revoltjs/FileUploads"; + import { ModalProps } from "../types"; const Preview = styled(Centred)` diff --git a/src/context/modals/components/ShowToken.tsx b/src/controllers/modals/components/ShowToken.tsx similarity index 100% rename from src/context/modals/components/ShowToken.tsx rename to src/controllers/modals/components/ShowToken.tsx diff --git a/src/context/modals/components/SignOutSessions.tsx b/src/controllers/modals/components/SignOutSessions.tsx similarity index 100% rename from src/context/modals/components/SignOutSessions.tsx rename to src/controllers/modals/components/SignOutSessions.tsx diff --git a/src/context/modals/components/SignedOut.tsx b/src/controllers/modals/components/SignedOut.tsx similarity index 100% rename from src/context/modals/components/SignedOut.tsx rename to src/controllers/modals/components/SignedOut.tsx diff --git a/src/context/modals/types.ts b/src/controllers/modals/types.ts similarity index 100% rename from src/context/modals/types.ts rename to src/controllers/modals/types.ts diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 5028bddd..2dc652d7 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -28,7 +28,6 @@ import { QueuedMessage } from "../mobx/stores/MessageQueue"; import { NotificationState } from "../mobx/stores/NotificationOptions"; import { Screen, useIntermediate } from "../context/intermediate/Intermediate"; -import { modalController } from "../context/modals"; import { AppContext, ClientStatus, @@ -39,6 +38,7 @@ import CMNotifications from "./contextmenu/CMNotifications"; import Tooltip from "../components/common/Tooltip"; import UserStatus from "../components/common/user/UserStatus"; +import { modalController } from "../controllers/modals/ModalController"; import { internalEmit } from "./eventEmitter"; import { getRenderer } from "./renderer/Singleton"; diff --git a/src/mobx/stores/Changelog.ts b/src/mobx/stores/Changelog.ts index 410e1518..553117e0 100644 --- a/src/mobx/stores/Changelog.ts +++ b/src/mobx/stores/Changelog.ts @@ -1,8 +1,7 @@ import { action, makeAutoObservable, runInAction } from "mobx"; -import { modalController } from "../../context/modals"; - import { latestChangelog } from "../../assets/changelogs"; +import { modalController } from "../../controllers/modals/ModalController"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; import Syncable from "../interfaces/Syncable"; diff --git a/src/pages/Open.tsx b/src/pages/Open.tsx index 20e5265f..40dd5f7a 100644 --- a/src/pages/Open.tsx +++ b/src/pages/Open.tsx @@ -6,13 +6,14 @@ import { useContext, useEffect } from "preact/hooks"; import { Header } from "@revoltchat/ui"; -import { modalController } from "../context/modals"; import { AppContext, ClientStatus, StatusContext, } from "../context/revoltjs/RevoltClient"; +import { modalController } from "../controllers/modals/ModalController"; + export default function Open() { const history = useHistory(); const client = useContext(AppContext); diff --git a/src/pages/friends/Friends.tsx b/src/pages/friends/Friends.tsx index aa3af078..e269755f 100644 --- a/src/pages/friends/Friends.tsx +++ b/src/pages/friends/Friends.tsx @@ -13,13 +13,13 @@ import { TextReact } from "../../lib/i18n"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { modalController } from "../../context/modals"; 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 { modalController } from "../../controllers/modals/ModalController"; import { Friend } from "./Friend"; export default observer(() => { diff --git a/src/pages/login/forms/FormLogin.tsx b/src/pages/login/forms/FormLogin.tsx index 6fc90852..95b2971a 100644 --- a/src/pages/login/forms/FormLogin.tsx +++ b/src/pages/login/forms/FormLogin.tsx @@ -4,8 +4,8 @@ import { API } from "revolt.js"; import { useApplicationState } from "../../../mobx/State"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { modalController } from "../../../context/modals"; +import { modalController } from "../../../controllers/modals/ModalController"; import { Form } from "./Form"; export function FormLogin() { diff --git a/src/pages/settings/GenericSettings.tsx b/src/pages/settings/GenericSettings.tsx index 0c6e5d94..ce2ff32f 100644 --- a/src/pages/settings/GenericSettings.tsx +++ b/src/pages/settings/GenericSettings.tsx @@ -13,9 +13,8 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../mobx/State"; -import { modalController } from "../../context/modals"; - import ButtonItem from "../../components/navigation/items/ButtonItem"; +import { modalController } from "../../controllers/modals/ModalController"; interface Props { pages: { diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 7b0f5c36..e5628874 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -35,7 +35,6 @@ import { LineDivider } from "@revoltchat/ui"; import { useApplicationState } from "../../mobx/State"; import { useIntermediate } from "../../context/intermediate/Intermediate"; -import { modalController } from "../../context/modals"; import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import { AppContext, LogOutContext } from "../../context/revoltjs/RevoltClient"; @@ -43,6 +42,7 @@ import UserIcon from "../../components/common/user/UserIcon"; import { Username } from "../../components/common/user/UserShort"; import UserStatus from "../../components/common/user/UserStatus"; import ButtonItem from "../../components/navigation/items/ButtonItem"; +import { modalController } from "../../controllers/modals/ModalController"; import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../revision"; import { APP_VERSION } from "../../version"; import { GenericSettings } from "./GenericSettings"; diff --git a/src/pages/settings/panes/MyBots.tsx b/src/pages/settings/panes/MyBots.tsx index 3cb64855..226c4478 100644 --- a/src/pages/settings/panes/MyBots.tsx +++ b/src/pages/settings/panes/MyBots.tsx @@ -25,7 +25,6 @@ import { useTranslation } from "../../../lib/i18n"; import { stopPropagation } from "../../../lib/stopPropagation"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { modalController } from "../../../context/modals"; import { FileUploader } from "../../../context/revoltjs/FileUploads"; import { useClient } from "../../../context/revoltjs/RevoltClient"; @@ -35,6 +34,7 @@ import AutoComplete, { import CollapsibleSection from "../../../components/common/CollapsibleSection"; import Tooltip from "../../../components/common/Tooltip"; import UserIcon from "../../../components/common/user/UserIcon"; +import { modalController } from "../../../controllers/modals/ModalController"; interface Data { _id: string; diff --git a/src/pages/settings/panes/Notifications.tsx b/src/pages/settings/panes/Notifications.tsx index d934a63d..a6bf472b 100644 --- a/src/pages/settings/panes/Notifications.tsx +++ b/src/pages/settings/panes/Notifications.tsx @@ -10,9 +10,10 @@ import { urlBase64ToUint8Array } from "../../../lib/conversion"; import { useApplicationState } from "../../../mobx/State"; -import { modalController } from "../../../context/modals"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; +import { modalController } from "../../../controllers/modals/ModalController"; + export const Notifications = observer(() => { const client = useContext(AppContext); const settings = useApplicationState().settings; diff --git a/src/pages/settings/panes/Sessions.tsx b/src/pages/settings/panes/Sessions.tsx index 73e39bc3..dcc55524 100644 --- a/src/pages/settings/panes/Sessions.tsx +++ b/src/pages/settings/panes/Sessions.tsx @@ -27,9 +27,10 @@ import { } from "@revoltchat/ui"; import { dayjs } from "../../../context/Locale"; -import { modalController } from "../../../context/modals"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; +import { modalController } from "../../../controllers/modals/ModalController"; + dayjs.extend(relativeTime); export function Sessions() { diff --git a/src/updateWorker.ts b/src/updateWorker.ts index 770a2a90..5043ef90 100644 --- a/src/updateWorker.ts +++ b/src/updateWorker.ts @@ -7,8 +7,7 @@ import { useEffect, useState } from "preact/hooks"; import { internalEmit, internalSubscribe } from "./lib/eventEmitter"; -import { modalController } from "./context/modals"; - +import { modalController } from "./controllers/modals/ModalController"; import { APP_VERSION } from "./version"; const INTERVAL_HOUR = 36e5; From 80f4bb3d9821482755c18fa65ad357b30565f2ed Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Tue, 28 Jun 2022 13:20:08 +0100 Subject: [PATCH 02/17] feat: build finite state machine for sessions --- src/controllers/client/ClientController.tsx | 49 ++++++ src/controllers/client/Session.tsx | 164 ++++++++++++++++++++ src/mobx/State.ts | 7 +- src/mobx/stores/Auth.ts | 37 ++--- src/mobx/stores/ServerConfig.ts | 4 +- src/types/revolt-api.d.ts | 2 + 6 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 src/controllers/client/ClientController.tsx create mode 100644 src/controllers/client/Session.tsx diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx new file mode 100644 index 00000000..25fc536c --- /dev/null +++ b/src/controllers/client/ClientController.tsx @@ -0,0 +1,49 @@ +import { action, makeAutoObservable, ObservableMap } from "mobx"; +import type { Nullable } from "revolt.js"; + +import Auth from "../../mobx/stores/Auth"; + +import Session from "./Session"; + +class ClientController { + /** + * Map of user IDs to sessions + */ + private sessions: ObservableMap; + + /** + * User ID of active session + */ + private current: Nullable; + + constructor() { + this.sessions = new ObservableMap(); + this.current = null; + + makeAutoObservable(this); + } + + /** + * Hydrate sessions and start client lifecycles. + * @param auth Authentication store + */ + @action hydrate(auth: Auth) { + for (const entry of auth.getAccounts()) { + const session = new Session(); + session.emit({ + action: "LOGIN", + session: entry.session, + }); + } + } + + getActiveSession() { + return this.sessions; + } + + isLoggedIn() { + return this.current === null; + } +} + +export const clientController = new ClientController(); diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx new file mode 100644 index 00000000..ec9c7e45 --- /dev/null +++ b/src/controllers/client/Session.tsx @@ -0,0 +1,164 @@ +import { action, makeAutoObservable } from "mobx"; +import { Client } from "revolt.js"; + +type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; + +type Transition = + | { + action: "LOGIN"; + session: SessionPrivate; + } + | { + action: + | "SUCCESS" + | "DISCONNECT" + | "RETRY" + | "LOGOUT" + | "ONLINE" + | "OFFLINE"; + }; + +export default class Session { + state: State = window.navigator.onLine ? "Ready" : "Offline"; + client: Client | null = null; + + 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); + } + + private onOnline() { + this.emit({ + action: "ONLINE", + }); + } + + private onOffline() { + this.emit({ + action: "OFFLINE", + }); + } + + private onDropped() { + this.emit({ + action: "DISCONNECT", + }); + } + + private onReady() { + this.emit({ + action: "SUCCESS", + }); + } + + private createClient() { + this.client = new Client({ + unreads: true, + autoReconnect: false, + onPongTimeout: "EXIT", + apiURL: import.meta.env.VITE_API_URL, + }); + + this.client.addListener("dropped", this.onDropped); + this.client.addListener("ready", this.onReady); + } + + private destroyClient() { + this.client!.removeAllListeners(); + this.client = null; + } + + 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})`; + } + } + + @action async emit(data: Transition) { + switch (data.action) { + // Login with session + case "LOGIN": { + this.assert("Ready"); + this.state = "Connecting"; + this.createClient(); + + try { + await this.client!.useExistingSession(data.session); + } catch (err) { + this.state = "Ready"; + throw err; + } + + 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(() => { + this.emit({ + action: "RETRY", + }); + }, 1500); + } + + 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; + } + } + } +} diff --git a/src/mobx/State.ts b/src/mobx/State.ts index e742ac44..97299b69 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -4,8 +4,7 @@ import localforage from "localforage"; import { makeAutoObservable, reaction, runInAction } from "mobx"; import { Client } from "revolt.js"; -import { reportError } from "../lib/ErrorBoundary"; - +import { clientController } from "../controllers/client/ClientController"; import Persistent from "./interfaces/Persistent"; import Syncable from "./interfaces/Syncable"; import Auth from "./stores/Auth"; @@ -24,6 +23,7 @@ import Sync, { Data as DataSync, SyncKeys } from "./stores/Sync"; export const MIGRATIONS = { REDUX: 1640305719826, + MULTI_SERVER_CONFIG: 1656350006152, }; /** @@ -253,6 +253,9 @@ export default class State { // Post-hydration, init plugins. this.plugins.init(); + + // Push authentication information forwards to client controller. + clientController.hydrate(this.auth); } /** diff --git a/src/mobx/stores/Auth.ts b/src/mobx/stores/Auth.ts index 71cea92c..1d63e7be 100644 --- a/src/mobx/stores/Auth.ts +++ b/src/mobx/stores/Auth.ts @@ -1,6 +1,4 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; -import { API } from "revolt.js"; -import { Nullable } from "revolt.js"; import { mapToRecord } from "../../lib/conversion"; @@ -13,7 +11,6 @@ interface Account { export interface Data { sessions: Record; - current?: string; } /** @@ -22,14 +19,12 @@ export interface Data { */ export default class Auth implements Store, Persistent { private sessions: ObservableMap; - private current: Nullable; /** * Construct new Auth store. */ constructor() { this.sessions = new ObservableMap(); - this.current = null; // Inject session token if it is provided. if (import.meta.env.VITE_SESSION_TOKEN) { @@ -40,8 +35,6 @@ export default class Auth implements Store, Persistent { token: import.meta.env.VITE_SESSION_TOKEN as string, }, }); - - this.current = "0"; } makeAutoObservable(this); @@ -54,7 +47,6 @@ export default class Auth implements Store, Persistent { @action toJSON() { return { sessions: JSON.parse(JSON.stringify(mapToRecord(this.sessions))), - current: this.current ?? undefined, }; } @@ -72,10 +64,6 @@ export default class Auth implements Store, Persistent { this.sessions.set(id, v[id]), ); } - - if (data.current && this.sessions.has(data.current)) { - this.current = data.current; - } } /** @@ -84,7 +72,6 @@ export default class Auth implements Store, Persistent { */ @action setSession(session: Session) { this.sessions.set(session.user_id, { session }); - this.current = session.user_id; } /** @@ -92,34 +79,38 @@ export default class Auth implements Store, Persistent { * @param user_id User ID tied to session */ @action removeSession(user_id: string) { - if (user_id == this.current) { - 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. */ - @action logout() { + /*@action logout() { this.current && this.removeSession(this.current); - } + }*/ /** * Get current session. * @returns Current session */ - @computed getSession() { + /*@computed getSession() { if (!this.current) return; return this.sessions.get(this.current)!.session; - } + }*/ /** * Check whether we are currently logged in. * @returns Whether we are logged in */ - @computed isLoggedIn() { + /*@computed isLoggedIn() { return this.current !== null; - } + }*/ } diff --git a/src/mobx/stores/ServerConfig.ts b/src/mobx/stores/ServerConfig.ts index 4212e82d..17ee1230 100644 --- a/src/mobx/stores/ServerConfig.ts +++ b/src/mobx/stores/ServerConfig.ts @@ -1,7 +1,5 @@ import { action, computed, makeAutoObservable } from "mobx"; -import { API } from "revolt.js"; -import { Client } from "revolt.js"; -import { Nullable } from "revolt.js"; +import { API, Client, Nullable } from "revolt.js"; import { isDebug } from "../../revision"; import Persistent from "../interfaces/Persistent"; diff --git a/src/types/revolt-api.d.ts b/src/types/revolt-api.d.ts index ae1bb349..577b1b52 100644 --- a/src/types/revolt-api.d.ts +++ b/src/types/revolt-api.d.ts @@ -5,3 +5,5 @@ declare type Session = { name: string; user_id: string; }; + +declare type SessionPrivate = Session; From ce88fab71455f2b1a36c1013690386e54e51d235 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Tue, 28 Jun 2022 13:49:50 +0100 Subject: [PATCH 03/17] feat: get fsm to a working testing state --- external/lang | 2 +- src/context/index.tsx | 2 +- src/context/revoltjs/RevoltClient.tsx | 21 +++++++++++++------ src/controllers/client/ClientController.tsx | 23 +++++++++++++++++---- src/controllers/client/Session.tsx | 23 ++++++++++++++++++++- src/mobx/stores/Auth.ts | 8 ++++--- 6 files changed, 63 insertions(+), 16 deletions(-) diff --git a/external/lang b/external/lang index def08f21..50838167 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit def08f210e9edc4f203cb38611fd270761102860 +Subproject commit 50838167d7d253de9d08715e6a6070c3ddc9fcc2 diff --git a/src/context/index.tsx b/src/context/index.tsx index 1785174b..5b30422c 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -20,7 +20,7 @@ const uiContext = { Link, Text: Text as any, Trigger: ContextMenuTrigger, - emitAction: () => {}, + emitAction: () => void {}, }; /** diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx index f16f3ac1..b76f4585 100644 --- a/src/context/revoltjs/RevoltClient.tsx +++ b/src/context/revoltjs/RevoltClient.tsx @@ -9,6 +9,7 @@ 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"; @@ -36,8 +37,8 @@ type Props = { }; export default observer(({ children }: Props) => { - const state = useApplicationState(); - const [client, setClient] = useState(null!); + // const state = useApplicationState(); + /*const [client, setClient] = useState(null!); const [status, setStatus] = useState(ClientStatus.LOADING); const [loaded, setLoaded] = useState(false); @@ -84,16 +85,24 @@ export default observer(({ children }: Props) => { }, [state.auth.getSession()]); useEffect(() => registerEvents(state, setStatus, client), [client]); - useEffect(() => state.registerListeners(client), [client]); if (!loaded || status === ClientStatus.LOADING) { return ; + }*/ + + const session = clientController.getActiveSession(); + if (!session?.ready) { + return ; } + const client = session.client!; + const state = useApplicationState(); + useEffect(() => state.registerListeners(client), [state, client]); + return ( - - - + + + void {}}> {children} diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index 25fc536c..56200d97 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -1,4 +1,4 @@ -import { action, makeAutoObservable, ObservableMap } from "mobx"; +import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; import type { Nullable } from "revolt.js"; import Auth from "../../mobx/stores/Auth"; @@ -30,20 +30,35 @@ class ClientController { @action hydrate(auth: Auth) { for (const entry of auth.getAccounts()) { const session = new Session(); + this.sessions.set(entry.session._id!, session); session.emit({ action: "LOGIN", session: entry.session, }); } + + this.current = this.sessions.keys().next().value ?? null; } - getActiveSession() { - return this.sessions; + @computed getActiveSession() { + return this.sessions.get(this.current!); } - isLoggedIn() { + @computed isLoggedIn() { return this.current === null; } + + @action logout(user_id: string) { + const session = this.sessions.get(user_id); + if (session) { + this.sessions.delete(user_id); + if (user_id === this.current) { + this.current = this.sessions.keys().next().value ?? null; + } + + session.destroy(); + } + } } export const clientController = new ClientController(); diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index ec9c7e45..1189e059 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -1,4 +1,4 @@ -import { action, makeAutoObservable } from "mobx"; +import { action, computed, makeAutoObservable } from "mobx"; import { Client } from "revolt.js"; type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; @@ -34,6 +34,17 @@ export default class Session { 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; + } + } + private onOnline() { this.emit({ action: "ONLINE", @@ -90,6 +101,8 @@ export default class Session { } @action async emit(data: Transition) { + console.info("Handle event:", data); + switch (data.action) { // Login with session case "LOGIN": { @@ -161,4 +174,12 @@ export default class Session { } } } + + /** + * Whether we are ready to render. + * @returns Boolean + */ + @computed get ready() { + return this.client?.user; + } } diff --git a/src/mobx/stores/Auth.ts b/src/mobx/stores/Auth.ts index 1d63e7be..33799f7e 100644 --- a/src/mobx/stores/Auth.ts +++ b/src/mobx/stores/Auth.ts @@ -2,6 +2,7 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; import { mapToRecord } from "../../lib/conversion"; +import { clientController } from "../../controllers/client/ClientController"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; @@ -110,7 +111,8 @@ export default class Auth implements Store, Persistent { * Check whether we are currently logged in. * @returns Whether we are logged in */ - /*@computed isLoggedIn() { - return this.current !== null; - }*/ + @computed isLoggedIn() { + // ! FIXME: temp proxy info + return clientController.getActiveSession()?.ready; + } } From 5f2311b09cd284b9cb94e300d2039dbd04a2797a Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Tue, 28 Jun 2022 19:59:58 +0100 Subject: [PATCH 04/17] feat: implement `useClient` from client controller --- package.json | 1 + src/components/common/AutoComplete.tsx | 3 +- src/components/common/ChannelIcon.tsx | 7 +- src/components/common/ServerIcon.tsx | 5 +- src/components/common/messaging/Message.tsx | 4 +- .../common/messaging/MessageBox.tsx | 4 +- .../messaging/attachments/Attachment.tsx | 7 +- .../attachments/AttachmentActions.tsx | 4 +- .../messaging/attachments/ImageFile.tsx | 5 +- .../common/messaging/attachments/TextFile.tsx | 13 +-- .../messaging/bars/MessageOverlayBar.tsx | 5 +- .../common/messaging/embed/Embed.tsx | 2 +- .../common/messaging/embed/EmbedInvite.tsx | 18 ++-- .../common/messaging/embed/EmbedMedia.tsx | 3 +- src/components/common/user/UserIcon.tsx | 5 +- src/components/common/user/UserShort.tsx | 2 +- src/components/markdown/Renderer.tsx | 4 +- .../navigation/BottomNavigation.tsx | 3 +- .../navigation/items/ConnectionStatus.tsx | 36 +++---- .../navigation/left/HomeSidebar.tsx | 4 +- .../navigation/left/ServerListSidebar.tsx | 3 +- .../navigation/left/ServerSidebar.tsx | 3 +- .../navigation/right/MemberSidebar.tsx | 16 ++- src/components/navigation/right/Search.tsx | 3 +- .../settings/account/AccountManagement.tsx | 9 +- .../settings/account/EditAccount.tsx | 17 ++- .../account/MultiFactorAuthentication.tsx | 18 ++-- src/context/intermediate/modals/Input.tsx | 4 +- src/context/intermediate/modals/Prompt.tsx | 6 +- .../intermediate/popovers/CreateBot.tsx | 6 +- .../intermediate/popovers/ImageViewer.tsx | 2 +- .../intermediate/popovers/UserPicker.tsx | 2 +- .../intermediate/popovers/UserProfile.tsx | 28 ++--- src/context/revoltjs/CheckAuth.tsx | 12 +-- src/context/revoltjs/FileUploads.tsx | 8 +- src/context/revoltjs/Notifications.tsx | 6 +- src/context/revoltjs/RequiresOnline.tsx | 11 +- src/context/revoltjs/RevoltClient.tsx | 102 ++---------------- src/context/revoltjs/StateMonitor.tsx | 39 ------- src/context/revoltjs/SyncManager.tsx | 2 +- src/context/revoltjs/events.ts | 15 +-- src/controllers/client/ClientController.tsx | 81 ++++++++++++-- src/controllers/client/Session.tsx | 5 +- .../modals/components/ModifyAccount.tsx | 8 +- src/lib/ContextMenus.tsx | 13 +-- src/lib/FakeClient.tsx | 13 --- src/mobx/State.ts | 8 ++ src/mobx/stores/MessageQueue.ts | 15 +++ src/mobx/stores/Settings.ts | 8 +- src/pages/Open.tsx | 15 +-- src/pages/RevoltApp.tsx | 2 - src/pages/app.tsx | 5 +- src/pages/channels/Channel.tsx | 3 +- src/pages/channels/messaging/MessageArea.tsx | 19 ++-- .../channels/messaging/MessageRenderer.tsx | 6 +- src/pages/channels/voice/VoiceHeader.tsx | 2 +- src/pages/developer/Developer.tsx | 7 +- src/pages/friends/Friends.tsx | 2 +- src/pages/home/Home.tsx | 6 +- src/pages/invite/Invite.tsx | 22 ++-- src/pages/invite/InviteBot.tsx | 3 +- src/pages/login/ConfirmDelete.tsx | 9 +- src/pages/login/forms/FormReset.tsx | 17 +-- src/pages/login/forms/FormVerify.tsx | 14 +-- src/pages/settings/ChannelSettings.tsx | 2 +- src/pages/settings/ServerSettings.tsx | 2 +- src/pages/settings/Settings.tsx | 11 +- src/pages/settings/panes/MyBots.tsx | 5 +- src/pages/settings/panes/Notifications.tsx | 7 +- src/pages/settings/panes/Profile.tsx | 14 +-- src/pages/settings/panes/Sessions.tsx | 6 +- yarn.lock | 10 ++ 72 files changed, 330 insertions(+), 457 deletions(-) delete mode 100644 src/context/revoltjs/StateMonitor.tsx delete mode 100644 src/lib/FakeClient.tsx 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() { + ); } else if (session.state === "Connecting") { diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index df3216bd..be9497ce 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -1,6 +1,8 @@ import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; import { Client, Nullable } from "revolt.js"; +import { injectController } from "../../lib/window"; + import Auth from "../../mobx/stores/Auth"; import { modalController } from "../modals/ModalController"; @@ -35,7 +37,7 @@ class ClientController { this.logoutCurrent = this.logoutCurrent.bind(this); // Inject globally - (window as any).clientController = this; + injectController("client", this); } /** @@ -53,12 +55,14 @@ class ClientController { .emit({ action: "LOGIN", session: entry.session, + apiUrl: entry.apiUrl, }) .catch((error) => { if (error === "Forbidden" || error === "Unauthorized") { this.sessions.delete(user_id); auth.removeSession(user_id); modalController.push({ type: "signed_out" }); + session.destroy(); } }); } diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index b19750e5..96c203b9 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -7,6 +7,7 @@ type Transition = | { action: "LOGIN"; session: SessionPrivate; + apiUrl?: string; } | { action: @@ -70,12 +71,12 @@ export default class Session { }); } - private createClient() { + private createClient(apiUrl?: string) { this.client = new Client({ unreads: true, autoReconnect: false, onPongTimeout: "EXIT", - apiURL: import.meta.env.VITE_API_URL, + apiURL: apiUrl ?? import.meta.env.VITE_API_URL, }); this.client.addListener("dropped", this.onDropped); @@ -110,7 +111,7 @@ export default class Session { case "LOGIN": { this.assert("Ready"); this.state = "Connecting"; - this.createClient(); + this.createClient(data.apiUrl); try { await this.client!.useExistingSession(data.session); @@ -135,10 +136,13 @@ export default class Session { this.state = "Disconnected"; setTimeout(() => { - this.emit({ - action: "RETRY", - }); - }, 1500); + // Check we are still disconnected before retrying. + if (this.state === "Disconnected") { + this.emit({ + action: "RETRY", + }); + } + }, 1000); } break; diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx index 48bb810d..8102cab8 100644 --- a/src/controllers/modals/ModalController.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -9,6 +9,7 @@ import type { Client, API } from "revolt.js"; import { ulid } from "ulid"; import { determineLink } from "../../lib/links"; +import { injectController } from "../../lib/window"; import { getApplicationState } from "../../mobx/State"; @@ -52,6 +53,9 @@ class ModalController { rendered: computed, isVisible: computed, }); + + // Inject globally + injectController("modal", this); } /** diff --git a/src/lib/window.ts b/src/lib/window.ts new file mode 100644 index 00000000..f1290106 --- /dev/null +++ b/src/lib/window.ts @@ -0,0 +1,6 @@ +export function injectController(key: string, value: any) { + (window as any).controllers = { + ...((window as any).controllers ?? {}), + [key]: value, + }; +} diff --git a/src/mobx/stores/Auth.ts b/src/mobx/stores/Auth.ts index 33799f7e..12574350 100644 --- a/src/mobx/stores/Auth.ts +++ b/src/mobx/stores/Auth.ts @@ -8,6 +8,7 @@ import Store from "../interfaces/Store"; interface Account { session: Session; + apiUrl?: string; } export interface Data { @@ -70,9 +71,10 @@ export default class Auth implements Store, Persistent { /** * Add a new session to the auth manager. * @param session Session + * @param apiUrl Custom API URL */ - @action setSession(session: Session) { - this.sessions.set(session.user_id, { session }); + @action setSession(session: Session, apiUrl?: string) { + this.sessions.set(session.user_id, { session, apiUrl }); } /** From 66ae518e51464fa43f9db23322c4a5201a9ce118 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 10:52:42 +0100 Subject: [PATCH 06/17] feat: make login functional again --- src/controllers/client/ClientController.tsx | 162 +++++++++++++++++--- src/controllers/client/Session.tsx | 9 +- src/mobx/State.ts | 4 +- src/pages/login/forms/CaptchaBlock.tsx | 4 +- src/pages/login/forms/Form.tsx | 11 +- src/pages/login/forms/FormCreate.tsx | 6 +- src/pages/login/forms/FormLogin.tsx | 104 +------------ 7 files changed, 160 insertions(+), 140 deletions(-) diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index be9497ce..9939df17 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -1,8 +1,10 @@ +import { detect } from "detect-browser"; import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; -import { Client, Nullable } from "revolt.js"; +import { API, Client, Nullable } from "revolt.js"; import { injectController } from "../../lib/window"; +import { state } from "../../mobx/State"; import Auth from "../../mobx/stores/Auth"; import { modalController } from "../modals/ModalController"; @@ -14,6 +16,11 @@ class ClientController { */ private apiClient: Client; + /** + * Server configuration + */ + private configuration: API.RevoltConfig | null; + /** * Map of user IDs to sessions */ @@ -29,45 +36,38 @@ class ClientController { apiURL: import.meta.env.VITE_API_URL, }); + 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.current = + 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()) { - const user_id = entry.session.user_id!; - - const session = new Session(); - this.sessions.set(user_id, session); - - session - .emit({ - action: "LOGIN", - session: entry.session, - apiUrl: entry.apiUrl, - }) - .catch((error) => { - if (error === "Forbidden" || error === "Unauthorized") { - this.sessions.delete(user_id); - auth.removeSession(user_id); - modalController.push({ type: "signed_out" }); - session.destroy(); - } - }); + this.addSession(entry); } - this.current = this.sessions.keys().next().value ?? null; + this.pickNextSession(); } @computed getActiveSession() { @@ -82,16 +82,134 @@ class ClientController { return this.getActiveSession()?.client ?? this.apiClient; } + @computed getServerConfig() { + return this.configuration; + } + @computed isLoggedIn() { return this.current === null; } + @action addSession(entry: { session: SessionPrivate; apiUrl?: string }) { + const user_id = entry.session.user_id!; + + const session = new Session(); + this.sessions.set(user_id, session); + + session + .emit({ + action: "LOGIN", + session: entry.session, + apiUrl: entry.apiUrl, + configuration: this.configuration!, + }) + .catch((error) => { + if (error === "Forbidden" || error === "Unauthorized") { + this.sessions.delete(user_id); + state.auth.removeSession(user_id); + modalController.push({ type: "signed_out" }); + session.destroy(); + } + }); + + this.pickNextSession(); + } + + 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; + 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") { + throw "Cancelled"; + } + + session = await this.apiClient.api.post("/auth/session/login", { + mfa_response, + mfa_ticket: session.ticket, + friendly_name, + }); + + if (session.result === "MFA") { + // unreachable code + return; + } + } + + this.addSession({ + session, + }); + + /*const s = session; + + client.session = session; + (client as any).$updateHeaders(); + + async function login() { + state.auth.setSession(s); + } + + const { onboarding } = await client.api.get("/onboard/hello"); + + if (onboarding) { + openScreen({ + id: "onboarding", + callback: async (username: string) => + client.completeOnboarding({ username }, false).then(login), + }); + } else { + login(); + }*/ + } + @action logout(user_id: string) { const session = this.sessions.get(user_id); if (session) { this.sessions.delete(user_id); if (user_id === this.current) { - this.current = this.sessions.keys().next().value ?? null; + this.current = null; + this.pickNextSession(); } session.destroy(); diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index 96c203b9..e2e66b10 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -1,13 +1,14 @@ import { action, computed, makeAutoObservable } from "mobx"; -import { Client } from "revolt.js"; +import { API, Client } from "revolt.js"; type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; type Transition = | { action: "LOGIN"; - session: SessionPrivate; apiUrl?: string; + session: SessionPrivate; + configuration?: API.RevoltConfig; } | { action: @@ -113,6 +114,10 @@ export default class Session { this.state = "Connecting"; this.createClient(data.apiUrl); + if (data.configuration) { + this.client!.configuration = data.configuration; + } + try { await this.client!.useExistingSession(data.session); this.user_id = this.client!.user!._id; diff --git a/src/mobx/State.ts b/src/mobx/State.ts index 7a951ab9..ea1f1212 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -36,7 +36,7 @@ export default class State { locale: LocaleOptions; experiments: Experiments; layout: Layout; - config: ServerConfig; + private config: ServerConfig; notifications: NotificationOptions; queue: MessageQueue; settings: Settings; @@ -288,7 +288,7 @@ export default class State { } } -let state: State; +export let state: State; export async function hydrateState() { state = new State(); diff --git a/src/pages/login/forms/CaptchaBlock.tsx b/src/pages/login/forms/CaptchaBlock.tsx index 9dc8888c..c91d75e1 100644 --- a/src/pages/login/forms/CaptchaBlock.tsx +++ b/src/pages/login/forms/CaptchaBlock.tsx @@ -7,7 +7,7 @@ import { useEffect } from "preact/hooks"; import { Preloader } from "@revoltchat/ui"; -import { useApplicationState } from "../../../mobx/State"; +import { clientController } from "../../../controllers/client/ClientController"; export interface CaptchaProps { onSuccess: (token?: string) => void; @@ -15,7 +15,7 @@ export interface CaptchaProps { } export const CaptchaBlock = observer((props: CaptchaProps) => { - const configuration = useApplicationState().config.get(); + const configuration = clientController.getServerConfig(); useEffect(() => { if (!configuration?.features.captcha.enabled) { diff --git a/src/pages/login/forms/Form.tsx b/src/pages/login/forms/Form.tsx index 2ec8d0eb..2a1b7d6b 100644 --- a/src/pages/login/forms/Form.tsx +++ b/src/pages/login/forms/Form.tsx @@ -6,16 +6,14 @@ import styles from "../Login.module.scss"; import { Text } from "preact-i18n"; import { useState } from "preact/hooks"; -import { Button, Category, Preloader } from "@revoltchat/ui"; -import { Tip } from "@revoltchat/ui"; - -import { useApplicationState } from "../../../mobx/State"; +import { Button, Category, Preloader, Tip } from "@revoltchat/ui"; import { I18nError } from "../../../context/Locale"; import { takeError } from "../../../context/revoltjs/util"; import WaveSVG from "../../settings/assets/wave.svg"; +import { clientController } from "../../../controllers/client/ClientController"; import FormField from "../FormField"; import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock"; import { MailProvider } from "./MailProvider"; @@ -45,7 +43,7 @@ interface FormInputs { } export const Form = observer(({ page, callback }: Props) => { - const configuration = useApplicationState().config.get(); + const configuration = clientController.getServerConfig(); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(undefined); @@ -260,7 +258,8 @@ export const Form = observer(({ page, callback }: Props) => { + target="_blank" + rel="noreferrer"> diff --git a/src/pages/login/forms/FormCreate.tsx b/src/pages/login/forms/FormCreate.tsx index 8d2e9ed7..65b4305f 100644 --- a/src/pages/login/forms/FormCreate.tsx +++ b/src/pages/login/forms/FormCreate.tsx @@ -1,9 +1,7 @@ -import { useApplicationState } from "../../../mobx/State"; - +import { useClient } from "../../../controllers/client/ClientController"; import { Form } from "./Form"; export function FormCreate() { - const config = useApplicationState().config; - const client = config.createClient(); + const client = useClient(); return client.register(data)} />; } diff --git a/src/pages/login/forms/FormLogin.tsx b/src/pages/login/forms/FormLogin.tsx index 95b2971a..d32db521 100644 --- a/src/pages/login/forms/FormLogin.tsx +++ b/src/pages/login/forms/FormLogin.tsx @@ -1,106 +1,6 @@ -import { detect } from "detect-browser"; -import { API } from "revolt.js"; - -import { useApplicationState } from "../../../mobx/State"; - -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - -import { modalController } from "../../../controllers/modals/ModalController"; +import { clientController } from "../../../controllers/client/ClientController"; import { Form } from "./Form"; export function FormLogin() { - const state = useApplicationState(); - const { openScreen } = useIntermediate(); - - return ( - { - const browser = detect(); - 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"; - } - - // ! FIXME: temporary login flow code - // This should be replaced in the future. - const client = state.config.createClient(); - await client.fetchConfiguration(); - - let session = await client.api.post("/auth/session/login", { - ...data, - friendly_name, - }); - - if (session.result === "MFA") { - const { allowed_methods } = session; - 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") { - throw "Cancelled"; - } - - session = await client.api.post("/auth/session/login", { - mfa_response, - mfa_ticket: session.ticket, - friendly_name, - }); - - if (session.result === "MFA") { - // unreachable code - return; - } - } - - const s = session; - - client.session = session; - (client as any).$updateHeaders(); - - async function login() { - state.auth.setSession(s); - } - - const { onboarding } = await client.api.get("/onboard/hello"); - - if (onboarding) { - openScreen({ - id: "onboarding", - callback: async (username: string) => - client - .completeOnboarding({ username }, false) - .then(login), - }); - } else { - login(); - } - }} - /> - ); + return ; } From 31220db8fe545d0b88235f3d2b8774ec335b3901 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 11:48:48 +0100 Subject: [PATCH 07/17] feat: fully working onboarding on login --- src/context/revoltjs/SyncManager.tsx | 12 ++++-- src/controllers/client/ClientController.tsx | 17 +++++--- src/controllers/client/Session.tsx | 46 ++++++++++++++++++--- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx index 5d72f8c2..5022a373 100644 --- a/src/context/revoltjs/SyncManager.tsx +++ b/src/context/revoltjs/SyncManager.tsx @@ -9,21 +9,25 @@ import { reportError } from "../../lib/ErrorBoundary"; import { useApplicationState } from "../../mobx/State"; -import { useClient } from "../../controllers/client/ClientController"; +import { + useClient, + useSession, +} from "../../controllers/client/ClientController"; export default function SyncManager() { const client = useClient(); + const session = useSession(); const state = useApplicationState(); // Sync settings from Revolt. useEffect(() => { - if (client) { + if (session?.ready) { state.sync - .pull(client) + .pull(session.client!) .catch(console.error) .finally(() => state.changelog.checkForUpdates()); } - }, [client]); + }, [session?.ready]); // Take data updates from Revolt. useEffect(() => { diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index 9939df17..3012ba35 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -64,7 +64,7 @@ class ClientController { */ @action hydrate(auth: Auth) { for (const entry of auth.getAccounts()) { - this.addSession(entry); + this.addSession(entry, "existing"); } this.pickNextSession(); @@ -90,7 +90,10 @@ class ClientController { return this.current === null; } - @action addSession(entry: { session: SessionPrivate; apiUrl?: string }) { + @action addSession( + entry: { session: SessionPrivate; apiUrl?: string }, + knowledge: "new" | "existing", + ) { const user_id = entry.session.user_id!; const session = new Session(); @@ -102,6 +105,7 @@ class ClientController { session: entry.session, apiUrl: entry.apiUrl, configuration: this.configuration!, + knowledge, }) .catch((error) => { if (error === "Forbidden" || error === "Unauthorized") { @@ -177,9 +181,12 @@ class ClientController { } } - this.addSession({ - session, - }); + this.addSession( + { + session, + }, + "new", + ); /*const s = session; diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index e2e66b10..d171613b 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -1,6 +1,10 @@ import { action, computed, makeAutoObservable } from "mobx"; import { API, Client } from "revolt.js"; +import { state } from "../../mobx/State"; + +import { __thisIsAHack } from "../../context/intermediate/Intermediate"; + type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; type Transition = @@ -9,6 +13,8 @@ type Transition = apiUrl?: string; session: SessionPrivate; configuration?: API.RevoltConfig; + + knowledge: "new" | "existing"; } | { action: @@ -104,6 +110,17 @@ export default class Session { } } + 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; + } + } + @action async emit(data: Transition) { console.info(`[FSM ${this.user_id ?? "Anonymous"}]`, data); @@ -118,14 +135,31 @@ export default class Session { this.client!.configuration = data.configuration; } - try { - await this.client!.useExistingSession(data.session); - this.user_id = this.client!.user!._id; - } catch (err) { - this.state = "Ready"; - throw err; + 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) { + __thisIsAHack({ + id: "onboarding", + callback: async (username: string) => + this.client!.completeOnboarding( + { username }, + false, + ).then(() => this.continueLogin(data)), + }); + + return; + } } + this.continueLogin(data); + break; } // Ready successfully received From 0e86f19da2bbcab092f5b62878a440b474510a16 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 14:49:48 +0100 Subject: [PATCH 08/17] chore(doc): document client controller --- src/controllers/client/ClientController.tsx | 66 ++++++++++++++------- src/controllers/client/Session.tsx | 46 +++++++++++++- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index 3012ba35..cc8b9449 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -10,6 +10,9 @@ import Auth from "../../mobx/stores/Auth"; import { modalController } from "../modals/ModalController"; import Session from "./Session"; +/** + * Controls the lifecycles of clients + */ class ClientController { /** * API client @@ -36,6 +39,7 @@ class ClientController { apiURL: import.meta.env.VITE_API_URL, }); + // ! FIXME: loop until success infinitely this.apiClient .fetchConfiguration() .then(() => (this.configuration = this.apiClient.configuration!)); @@ -70,26 +74,51 @@ class ClientController { this.pickNextSession(); } + /** + * Get the currently selected session + * @returns Active Session + */ @computed getActiveSession() { return this.sessions.get(this.current!); } + /** + * 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; } + /** + * 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", @@ -119,6 +148,10 @@ class ClientController { this.pickNextSession(); } + /** + * Login given a set of credentials + * @param credentials Credentials + */ async login(credentials: API.DataLogin) { const browser = detect(); @@ -181,35 +214,19 @@ class ClientController { } } + // Start client lifecycle this.addSession( { session, }, "new", ); - - /*const s = session; - - client.session = session; - (client as any).$updateHeaders(); - - async function login() { - state.auth.setSession(s); - } - - const { onboarding } = await client.api.get("/onboard/hello"); - - if (onboarding) { - openScreen({ - id: "onboarding", - callback: async (username: string) => - client.completeOnboarding({ username }, false).then(login), - }); - } else { - login(); - }*/ } + /** + * 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) { @@ -223,12 +240,19 @@ class ClientController { } } + /** + * 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; } diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index d171613b..dcd705ca 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -5,8 +5,14 @@ import { state } from "../../mobx/State"; import { __thisIsAHack } from "../../context/intermediate/Intermediate"; +/** + * Current lifecycle state + */ type State = "Ready" | "Connecting" | "Online" | "Disconnected" | "Offline"; +/** + * Possible transitions between states + */ type Transition = | { action: "LOGIN"; @@ -26,11 +32,17 @@ type Transition = | "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); @@ -44,7 +56,7 @@ export default class Session { } /** - * Initiate logout and destroy client. + * Initiate logout and destroy client */ @action destroy() { if (this.client) { @@ -54,30 +66,46 @@ export default class Session { } } + /** + * 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, @@ -90,12 +118,20 @@ export default class Session { 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) { @@ -110,6 +146,10 @@ export default class Session { } } + /** + * 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); @@ -121,6 +161,10 @@ export default class Session { } } + /** + * 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); From 0261fec676228bf8e43072ad813bf407d5fe5df8 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 16:02:35 +0100 Subject: [PATCH 09/17] chore: deprecate `RevoltClient` context --- .../navigation/right/MemberSidebar.tsx | 2 +- src/context/index.tsx | 8 +-- src/context/revoltjs/CheckAuth.tsx | 14 ++-- src/context/revoltjs/RevoltClient.tsx | 27 -------- src/controllers/client/ClientController.tsx | 27 ++++++-- src/controllers/client/Session.tsx | 2 +- src/controllers/client/jsx/Binder.tsx | 27 ++++++++ src/lib/renderer/Singleton.ts | 4 +- src/mobx/State.ts | 16 ++--- src/mobx/stores/Ordering.ts | 8 ++- src/mobx/stores/Plugins.ts | 18 ++--- src/pages/RevoltApp.tsx | 6 -- src/pages/app.tsx | 67 ++++++++++--------- 13 files changed, 118 insertions(+), 108 deletions(-) delete mode 100644 src/context/revoltjs/RevoltClient.tsx create mode 100644 src/controllers/client/jsx/Binder.tsx diff --git a/src/components/navigation/right/MemberSidebar.tsx b/src/components/navigation/right/MemberSidebar.tsx index 3dbf1f63..88e71b95 100644 --- a/src/components/navigation/right/MemberSidebar.tsx +++ b/src/components/navigation/right/MemberSidebar.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { useParams } from "react-router-dom"; import { Channel, Server, User, API } from "revolt.js"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect, useLayoutEffect, useState } from "preact/hooks"; import { useSession, diff --git a/src/context/index.tsx b/src/context/index.tsx index 5b30422c..cc3fcdd1 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -8,12 +8,12 @@ import { Preloader, UIProvider } from "@revoltchat/ui"; import { hydrateState } from "../mobx/State"; +import Binder from "../controllers/client/jsx/Binder"; import ModalRenderer from "../controllers/modals/ModalRenderer"; import Locale from "./Locale"; import Theme from "./Theme"; import { history } from "./history"; import Intermediate from "./intermediate/Intermediate"; -import Client from "./revoltjs/RevoltClient"; import SyncManager from "./revoltjs/SyncManager"; const uiContext = { @@ -41,10 +41,10 @@ export default function Context({ children }: { children: Children }) { - + {children} - - + {} + diff --git a/src/context/revoltjs/CheckAuth.tsx b/src/context/revoltjs/CheckAuth.tsx index e1dbcc55..126ff925 100644 --- a/src/context/revoltjs/CheckAuth.tsx +++ b/src/context/revoltjs/CheckAuth.tsx @@ -1,6 +1,7 @@ +import { observer } from "mobx-react-lite"; import { Redirect } from "react-router-dom"; -import { useSession } from "../../controllers/client/ClientController"; +import { clientController } from "../../controllers/client/ClientController"; interface Props { auth?: boolean; @@ -9,16 +10,17 @@ interface Props { children: Children; } -export const CheckAuth = (props: Props) => { - const session = useSession(); +export const CheckAuth = observer((props: Props) => { + const loggedIn = clientController.isLoggedIn(); - if (props.auth && !session?.ready) { + // Redirect if logged out on authenticated page or vice-versa. + if (props.auth && !loggedIn) { if (props.blockRender) return null; return ; - } else if (!props.auth && session?.ready) { + } else if (!props.auth && loggedIn) { if (props.blockRender) return null; return ; } return <>{props.children}; -}; +}); diff --git a/src/context/revoltjs/RevoltClient.tsx b/src/context/revoltjs/RevoltClient.tsx deleted file mode 100644 index 145a2550..00000000 --- a/src/context/revoltjs/RevoltClient.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { observer } from "mobx-react-lite"; - -import { useEffect } from "preact/hooks"; - -import { Preloader } from "@revoltchat/ui"; - -import { useApplicationState } from "../../mobx/State"; - -import { clientController } from "../../controllers/client/ClientController"; - -type Props = { - children: Children; -}; - -export default observer(({ children }: Props) => { - const session = clientController.getActiveSession(); - if (session) { - if (!session.ready) return ; - - const client = session.client!; - const state = useApplicationState(); - useEffect(() => state.registerListeners(client), [state, client]); - } - - return <>{children}; -}); diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index cc8b9449..4666ae5d 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -58,8 +58,9 @@ class ClientController { } @action pickNextSession() { - this.current = - this.current ?? this.sessions.keys().next().value ?? null; + this.switchAccount( + this.current ?? this.sessions.keys().next().value ?? null, + ); } /** @@ -82,6 +83,15 @@ class ClientController { 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 @@ -111,7 +121,15 @@ class ClientController { * @returns Whether we are logged in */ @computed isLoggedIn() { - return this.current === null; + return this.current !== null; + } + + /** + * Check whether we are currently ready + * @returns Whether we are ready to render + */ + @computed isReady() { + return this.getActiveSession()?.ready; } /** @@ -127,6 +145,7 @@ class ClientController { const session = new Session(); this.sessions.set(user_id, session); + this.pickNextSession(); session .emit({ @@ -144,8 +163,6 @@ class ClientController { session.destroy(); } }); - - this.pickNextSession(); } /** diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index dcd705ca..11f80d71 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -270,6 +270,6 @@ export default class Session { * @returns Boolean */ @computed get ready() { - return this.client?.user; + return !!this.client?.user; } } diff --git a/src/controllers/client/jsx/Binder.tsx b/src/controllers/client/jsx/Binder.tsx new file mode 100644 index 00000000..6dea98ac --- /dev/null +++ b/src/controllers/client/jsx/Binder.tsx @@ -0,0 +1,27 @@ +import { observer } from "mobx-react-lite"; + +import { useEffect } from "preact/hooks"; + +import { Preloader } from "@revoltchat/ui"; + +import { state } from "../../../mobx/State"; + +import { clientController } from "../ClientController"; + +/** + * Prevent render until the client is ready to display. + * Also binds listeners from state to the current client. + */ +const Binder: React.FC = ({ children }) => { + const client = clientController.getReadyClient(); + useEffect(() => state.registerListeners(client!), [client]); + + // Block render if client is getting ready to work. + if (clientController.isLoggedIn() && !clientController.isReady()) { + return ; + } + + return <>{children}; +}; + +export default observer(Binder); diff --git a/src/lib/renderer/Singleton.ts b/src/lib/renderer/Singleton.ts index f30b9ec0..b59e7a4c 100644 --- a/src/lib/renderer/Singleton.ts +++ b/src/lib/renderer/Singleton.ts @@ -1,8 +1,6 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { action, makeAutoObservable } from "mobx"; -import { Channel } from "revolt.js"; -import { Message } from "revolt.js"; -import { Nullable } from "revolt.js"; +import { Channel, Message, Nullable } from "revolt.js"; import { SimpleRenderer } from "./simple/SimpleRenderer"; import { RendererRoutines, ScrollState } from "./types"; diff --git a/src/mobx/State.ts b/src/mobx/State.ts index ea1f1212..bdc22d4f 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -47,8 +47,6 @@ export default class State { private persistent: [string, Persistent][] = []; private disabled: Set = new Set(); - client?: Client; - /** * Construct new State. */ @@ -67,14 +65,10 @@ export default class State { this.plugins = new Plugins(this); this.ordering = new Ordering(this); - makeAutoObservable(this, { - client: false, - }); + makeAutoObservable(this); this.register(); this.setDisabled = this.setDisabled.bind(this); - - this.client = undefined; } /** @@ -138,11 +132,11 @@ export default class State { registerListeners(client?: Client) { // If a client is present currently, expose it and provide it to plugins. if (client) { - this.client = client; + // this.client = client; this.plugins.onClient(client); // Register message listener for clearing queue. - this.client.addListener("message", this.queue.onMessage); + // this.client.addListener("message", this.queue.onMessage); } // Register all the listeners required for saving and syncing state. @@ -228,13 +222,13 @@ export default class State { }); return () => { - // Remove any listeners attached to client. + /*// Remove any listeners attached to client. if (client) { client.removeListener("message", this.queue.onMessage); } // Stop exposing the client. - this.client = undefined; + this.client = undefined;*/ // Wipe all listeners. listeners.forEach((x) => x()); diff --git a/src/mobx/stores/Ordering.ts b/src/mobx/stores/Ordering.ts index 8e2ccb11..a72e2ddb 100644 --- a/src/mobx/stores/Ordering.ts +++ b/src/mobx/stores/Ordering.ts @@ -2,6 +2,7 @@ import { action, computed, makeAutoObservable } from "mobx"; import { reorder } from "@revoltchat/ui"; +import { clientController } from "../../controllers/client/ClientController"; import State from "../State"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; @@ -63,18 +64,19 @@ export default class Ordering implements Store, Persistent, Syncable { * All known servers with ordering applied */ @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 out = []; for (const id of ordered) { if (known.delete(id)) { - out.push(this.state.client!.servers.get(id)!); + out.push(client!.servers.get(id)!); } } for (const id of known) { - out.push(this.state.client!.servers.get(id)!); + out.push(client!.servers.get(id)!); } return out; diff --git a/src/mobx/stores/Plugins.ts b/src/mobx/stores/Plugins.ts index 08a480d5..711349e8 100644 --- a/src/mobx/stores/Plugins.ts +++ b/src/mobx/stores/Plugins.ts @@ -59,7 +59,7 @@ type Plugin = { type Instance = { format: 1; - onClient?: (client: Client) => {}; + onClient?: (client: Client) => void; onUnload?: () => void; }; @@ -124,7 +124,7 @@ export default class Plugins implements Store, Persistent { * @param id Plugin Id */ @computed get(namespace: string, id: string) { - return this.plugins.get(`${namespace }/${ id}`); + return this.plugins.get(`${namespace}/${id}`); } /** @@ -133,7 +133,7 @@ export default class Plugins implements Store, Persistent { * @returns Plugin Instance */ private getInstance(plugin: Pick) { - return this.instances.get(`${plugin.namespace }/${ plugin.id}`); + return this.instances.get(`${plugin.namespace}/${plugin.id}`); } /** @@ -159,7 +159,7 @@ export default class Plugins implements Store, Persistent { 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) { this.load(plugin.namespace, plugin.id); @@ -173,7 +173,7 @@ export default class Plugins implements Store, Persistent { */ remove(namespace: string, id: string) { this.unload(namespace, id); - this.plugins.delete(`${namespace }/${ id}`); + this.plugins.delete(`${namespace}/${id}`); } /** @@ -186,7 +186,7 @@ export default class Plugins implements Store, Persistent { if (!plugin) throw "Unknown plugin!"; try { - const ns = `${plugin.namespace }/${ plugin.id}`; + const ns = `${plugin.namespace}/${plugin.id}`; const instance: Instance = eval(plugin.entrypoint)(); this.instances.set(ns, { @@ -198,10 +198,6 @@ export default class Plugins implements Store, Persistent { ...plugin, enabled: true, }); - - if (this.state.client) { - instance.onClient?.(this.state.client); - } } catch (error) { console.error(`Failed to load ${namespace}/${id}!`); console.error(error); @@ -217,7 +213,7 @@ export default class Plugins implements Store, Persistent { const plugin = this.get(namespace, id); if (!plugin) throw "Unknown plugin!"; - const ns = `${plugin.namespace }/${ plugin.id}`; + const ns = `${plugin.namespace}/${plugin.id}`; const loaded = this.getInstance(plugin); if (loaded) { loaded.onUnload?.(); diff --git a/src/pages/RevoltApp.tsx b/src/pages/RevoltApp.tsx index 110b9b3a..69ccf6f5 100644 --- a/src/pages/RevoltApp.tsx +++ b/src/pages/RevoltApp.tsx @@ -76,12 +76,6 @@ const Routes = styled.div.attrs({ "data-component": "routes" })<{ background: var(--primary-background); - /*background-color: rgba( - var(--primary-background-rgb), - max(var(--min-opacity), 0.75) - );*/ - //backdrop-filter: blur(10px); - ${() => isTouchscreenDevice && css` diff --git a/src/pages/app.tsx b/src/pages/app.tsx index e50a27c8..e2853fad 100644 --- a/src/pages/app.tsx +++ b/src/pages/app.tsx @@ -15,44 +15,51 @@ const Login = lazy(() => import("./login/Login")); const ConfirmDelete = lazy(() => import("./login/ConfirmDelete")); const RevoltApp = lazy(() => import("./RevoltApp")); +const LoadSuspense: React.FC = ({ children }) => ( + // @ts-expect-error Typing issue between Preact and Preact. + }>{children} +); + export function App() { return ( - {/* - // @ts-expect-error typings mis-match between preact... and preact? */} - }> - - + + + + + + - - - - - + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - + + + + ); From 1fcb3cedc12fd2a922605ab185124d3036589576 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 16:27:57 +0100 Subject: [PATCH 10/17] feat: consistent authentication flow fix: missing suspense on login feat: re-prompt MFA if fail on login --- src/context/index.tsx | 7 ++- src/context/revoltjs/CheckAuth.tsx | 15 ++++++ src/controllers/client/ClientController.tsx | 55 +++++++++++++-------- src/controllers/client/jsx/Binder.tsx | 13 +---- src/pages/app.tsx | 4 +- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/context/index.tsx b/src/context/index.tsx index cc3fcdd1..045e733e 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -41,10 +41,9 @@ export default function Context({ children }: { children: Children }) { - - {children} - {} - + {children} + + diff --git a/src/context/revoltjs/CheckAuth.tsx b/src/context/revoltjs/CheckAuth.tsx index 126ff925..eec8e8e5 100644 --- a/src/context/revoltjs/CheckAuth.tsx +++ b/src/context/revoltjs/CheckAuth.tsx @@ -1,6 +1,8 @@ import { observer } from "mobx-react-lite"; import { Redirect } from "react-router-dom"; +import { Preloader } from "@revoltchat/ui"; + import { clientController } from "../../controllers/client/ClientController"; interface Props { @@ -10,6 +12,10 @@ interface Props { children: Children; } +/** + * Check that we are logged in or out and redirect accordingly. + * Also prevent render until the client is ready to display. + */ export const CheckAuth = observer((props: Props) => { const loggedIn = clientController.isLoggedIn(); @@ -22,5 +28,14 @@ export const CheckAuth = observer((props: Props) => { return ; } + // Block render if client is getting ready to work. + if ( + props.auth && + clientController.isLoggedIn() && + !clientController.isReady() + ) { + return ; + } + return <>{props.children}; }); diff --git a/src/controllers/client/ClientController.tsx b/src/controllers/client/ClientController.tsx index 4666ae5d..fc51c4f6 100644 --- a/src/controllers/client/ClientController.tsx +++ b/src/controllers/client/ClientController.tsx @@ -7,6 +7,7 @@ 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"; @@ -205,29 +206,37 @@ class ClientController { // Prompt for MFA verificaiton if necessary if (session.result === "MFA") { const { allowed_methods } = session; - const mfa_response: API.MFAResponse | undefined = await new Promise( - (callback) => - modalController.push({ - type: "mfa_flow", - state: "unknown", - available_methods: allowed_methods, - callback, - }), - ); + 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") { - throw "Cancelled"; + 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); + } } - session = await this.apiClient.api.post("/auth/session/login", { - mfa_response, - mfa_ticket: session.ticket, - friendly_name, - }); - if (session.result === "MFA") { - // unreachable code - return; + throw "Cancelled"; } } @@ -247,12 +256,12 @@ class ClientController { @action logout(user_id: string) { const session = this.sessions.get(user_id); if (session) { - this.sessions.delete(user_id); if (user_id === this.current) { this.current = null; - this.pickNextSession(); } + this.sessions.delete(user_id); + this.pickNextSession(); session.destroy(); } } @@ -272,6 +281,10 @@ class ClientController { */ @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(); } } diff --git a/src/controllers/client/jsx/Binder.tsx b/src/controllers/client/jsx/Binder.tsx index 6dea98ac..cbec7403 100644 --- a/src/controllers/client/jsx/Binder.tsx +++ b/src/controllers/client/jsx/Binder.tsx @@ -2,26 +2,17 @@ import { observer } from "mobx-react-lite"; import { useEffect } from "preact/hooks"; -import { Preloader } from "@revoltchat/ui"; - import { state } from "../../../mobx/State"; import { clientController } from "../ClientController"; /** - * Prevent render until the client is ready to display. * Also binds listeners from state to the current client. */ -const Binder: React.FC = ({ children }) => { +const Binder: React.FC = () => { const client = clientController.getReadyClient(); useEffect(() => state.registerListeners(client!), [client]); - - // Block render if client is getting ready to work. - if (clientController.isLoggedIn() && !clientController.isReady()) { - return ; - } - - return <>{children}; + return null; }; export default observer(Binder); diff --git a/src/pages/app.tsx b/src/pages/app.tsx index e2853fad..2251e942 100644 --- a/src/pages/app.tsx +++ b/src/pages/app.tsx @@ -49,7 +49,9 @@ export function App() { - + + + From 45692999bf63189b1c6a2d2db9123842ccbf34a6 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 16:41:26 +0100 Subject: [PATCH 11/17] chore(refactor): remove `SyncManager` --- src/context/index.tsx | 6 ++-- src/context/revoltjs/SyncManager.tsx | 50 -------------------------- src/lib/window.ts | 14 ++++++++ src/mobx/State.ts | 53 +++++++++++++++++++--------- src/mobx/stores/Plugins.ts | 11 ------ 5 files changed, 52 insertions(+), 82 deletions(-) delete mode 100644 src/context/revoltjs/SyncManager.tsx diff --git a/src/context/index.tsx b/src/context/index.tsx index 045e733e..51b0564f 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from "preact/hooks"; 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"; @@ -14,7 +14,6 @@ import Locale from "./Locale"; import Theme from "./Theme"; import { history } from "./history"; import Intermediate from "./intermediate/Intermediate"; -import SyncManager from "./revoltjs/SyncManager"; const uiContext = { Link, @@ -31,7 +30,7 @@ export default function Context({ children }: { children: Children }) { const [ready, setReady] = useState(false); useEffect(() => { - hydrateState().then(() => setReady(true)); + state.hydrate().then(() => setReady(true)); }, []); if (!ready) return ; @@ -42,7 +41,6 @@ export default function Context({ children }: { children: Children }) { {children} - diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx deleted file mode 100644 index 5022a373..00000000 --- a/src/context/revoltjs/SyncManager.tsx +++ /dev/null @@ -1,50 +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, - useSession, -} from "../../controllers/client/ClientController"; - -export default function SyncManager() { - const client = useClient(); - const session = useSession(); - const state = useApplicationState(); - - // Sync settings from Revolt. - useEffect(() => { - if (session?.ready) { - state.sync - .pull(session.client!) - .catch(console.error) - .finally(() => state.changelog.checkForUpdates()); - } - }, [session?.ready]); - - // 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 <>; -} diff --git a/src/lib/window.ts b/src/lib/window.ts index f1290106..7ee0ce3f 100644 --- a/src/lib/window.ts +++ b/src/lib/window.ts @@ -1,3 +1,17 @@ +/** + * 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 ?? {}), diff --git a/src/mobx/State.ts b/src/mobx/State.ts index bdc22d4f..b48d9162 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -1,8 +1,11 @@ // @ts-expect-error No typings. import stringify from "json-stringify-deterministic"; import localforage from "localforage"; -import { makeAutoObservable, reaction, runInAction } from "mobx"; -import { Client } from "revolt.js"; +import { action, makeAutoObservable, reaction, runInAction } from "mobx"; +import { Client, ClientboundNotification } from "revolt.js"; + +import { reportError } from "../lib/ErrorBoundary"; +import { injectWindow } from "../lib/window"; import { clientController } from "../controllers/client/ClientController"; import Persistent from "./interfaces/Persistent"; @@ -69,6 +72,10 @@ export default class State { this.register(); this.setDisabled = this.setDisabled.bind(this); + this.onPacket = this.onPacket.bind(this); + + // Inject into window + injectWindow("state", this); } /** @@ -125,6 +132,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. * @returns Function to dispose of listeners @@ -132,11 +153,17 @@ export default class State { registerListeners(client?: Client) { // If a client is present currently, expose it and provide it to plugins. if (client) { - // this.client = client; - this.plugins.onClient(client); - // Register message listener for clearing queue. - // this.client.addListener("message", this.queue.onMessage); + client.addListener("message", this.queue.onMessage); + + // Register listener for incoming packets. + client.addListener("packet", this.onPacket); + + // 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. @@ -222,14 +249,12 @@ export default class State { }); return () => { - /*// Remove any listeners attached to client. + // Remove any listeners attached to client. if (client) { client.removeListener("message", this.queue.onMessage); + client.removeListener("packet", this.onPacket); } - // Stop exposing the client. - this.client = undefined;*/ - // Wipe all listeners. listeners.forEach((x) => x()); }; @@ -282,13 +307,7 @@ export default class State { } } -export let state: State; - -export async function hydrateState() { - state = new State(); - (window as any).state = state; - await state.hydrate(); -} +export const state = new State(); /** * Get the application state diff --git a/src/mobx/stores/Plugins.ts b/src/mobx/stores/Plugins.ts index 711349e8..e6b3bbb3 100644 --- a/src/mobx/stores/Plugins.ts +++ b/src/mobx/stores/Plugins.ts @@ -41,7 +41,6 @@ type Plugin = { * ```typescript * function (state: State) { * return { - * onClient: (client: Client) => {}, * onUnload: () => {} * } * } @@ -59,7 +58,6 @@ type Plugin = { type Instance = { format: 1; - onClient?: (client: Client) => void; onUnload?: () => void; }; @@ -231,13 +229,4 @@ export default class Plugins implements Store, Persistent { localforage.removeItem("revite:plugins"); window.location.reload(); } - - /** - * Push client through to plugins - */ - onClient(client: Client) { - for (const instance of this.instances.values()) { - instance.onClient?.(client); - } - } } From 05516c58233c49f259c37b7422d044b80de164b5 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 16:46:25 +0100 Subject: [PATCH 12/17] fix: hide push notifications on electron app --- src/pages/settings/panes/Notifications.tsx | 102 +++++++++++---------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/src/pages/settings/panes/Notifications.tsx b/src/pages/settings/panes/Notifications.tsx index 921beb7b..d2dcdee9 100644 --- a/src/pages/settings/panes/Notifications.tsx +++ b/src/pages/settings/panes/Notifications.tsx @@ -61,57 +61,59 @@ export const Notifications = observer(() => { settings.set("notifications:desktop", desktopEnabled); }} /> - - } - description={ - - } - onChange={async (pushEnabled) => { - try { - const reg = - await navigator.serviceWorker?.getRegistration(); - if (reg) { - if (pushEnabled) { - const sub = await reg.pushManager.subscribe( - { - userVisibleOnly: true, - applicationServerKey: - urlBase64ToUint8Array( - client.configuration!.vapid, - ), - }, - ); - - // tell the server we just subscribed - const json = sub.toJSON(); - if (json.keys) { - client.api.post("/push/subscribe", { - endpoint: sub.endpoint, - ...(json.keys as { - p256dh: string; - auth: string; - }), - }); - setPushEnabled(true); - } - } else { - const sub = - await reg.pushManager.getSubscription(); - sub?.unsubscribe(); - setPushEnabled(false); - - client.api.post("/push/unsubscribe"); - } - } - } catch (err) { - console.error("Failed to enable push!", err); + {!window.native && ( + } - }} - /> + description={ + + } + onChange={async (pushEnabled) => { + try { + const reg = + await navigator.serviceWorker?.getRegistration(); + if (reg) { + if (pushEnabled) { + const sub = + await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: + urlBase64ToUint8Array( + client.configuration! + .vapid, + ), + }); + + // tell the server we just subscribed + const json = sub.toJSON(); + if (json.keys) { + client.api.post("/push/subscribe", { + endpoint: sub.endpoint, + ...(json.keys as { + p256dh: string; + auth: string; + }), + }); + setPushEnabled(true); + } + } else { + const sub = + await reg.pushManager.getSubscription(); + sub?.unsubscribe(); + setPushEnabled(false); + + client.api.post("/push/unsubscribe"); + } + } + } catch (err) { + console.error("Failed to enable push!", err); + } + }} + /> + )}

From a2a52e237d7ac1629128becfa0630d859d6365ac Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 17:31:59 +0100 Subject: [PATCH 13/17] chore(refactor): remove `Notifications` component --- src/context/history.ts | 11 + src/context/revoltjs/Notifications.tsx | 296 ------------------------- src/mobx/State.ts | 27 ++- src/mobx/stores/NotificationOptions.ts | 283 ++++++++++++++++++++++- src/pages/RevoltApp.tsx | 2 - 5 files changed, 316 insertions(+), 303 deletions(-) delete mode 100644 src/context/revoltjs/Notifications.tsx diff --git a/src/context/history.ts b/src/context/history.ts index 5e816997..0e4d1268 100644 --- a/src/context/history.ts +++ b/src/context/history.ts @@ -3,3 +3,14 @@ import { createBrowserHistory } from "history"; export const history = createBrowserHistory({ 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], +}; diff --git a/src/context/revoltjs/Notifications.tsx b/src/context/revoltjs/Notifications.tsx deleted file mode 100644 index c1d144a1..00000000 --- a/src/context/revoltjs/Notifications.tsx +++ /dev/null @@ -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, useEffect } from "preact/hooks"; - -import { useTranslation } from "../../lib/i18n"; - -import { useApplicationState } from "../../mobx/State"; - -import { useClient } from "../../controllers/client/ClientController"; - -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 = useClient(); - 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 ( - - - - - - - - - - - - ); -} diff --git a/src/mobx/State.ts b/src/mobx/State.ts index b48d9162..02a60e7e 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -39,6 +39,9 @@ export default class State { locale: LocaleOptions; experiments: Experiments; layout: Layout; + /** + * DEPRECATED + */ private config: ServerConfig; notifications: NotificationOptions; queue: MessageQueue; @@ -61,7 +64,7 @@ export default class State { this.experiments = new Experiments(); this.layout = new Layout(); this.config = new ServerConfig(); - this.notifications = new NotificationOptions(); + this.notifications = new NotificationOptions(this); this.queue = new MessageQueue(); this.settings = new Settings(); this.sync = new Sync(this); @@ -159,6 +162,17 @@ export default class State { // 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) @@ -253,6 +267,15 @@ export default class State { 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. @@ -293,7 +316,7 @@ export default class State { this.draft = new Draft(); this.experiments = new Experiments(); this.layout = new Layout(); - this.notifications = new NotificationOptions(); + this.notifications = new NotificationOptions(this); this.queue = new MessageQueue(); this.settings = new Settings(); this.sync = new Sync(this); diff --git a/src/mobx/stores/NotificationOptions.ts b/src/mobx/stores/NotificationOptions.ts index 2b6285a2..91a5fe35 100644 --- a/src/mobx/stores/NotificationOptions.ts +++ b/src/mobx/stores/NotificationOptions.ts @@ -1,8 +1,14 @@ 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 { history, routeInformation } from "../../context/history"; + +import State from "../State"; import Persistent from "../interfaces/Persistent"; import Store from "../interfaces/Store"; import Syncable from "../interfaces/Syncable"; @@ -37,22 +43,54 @@ export interface Data { channel?: Record; } +/** + * 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. */ export default class NotificationOptions implements Store, Persistent, Syncable { + private state: State; + private activeNotifications: Record; + private server: ObservableMap; private channel: ObservableMap; /** * Construct new Experiments store. */ - constructor() { + constructor(state: State) { this.server = 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() { @@ -209,6 +247,245 @@ export default class NotificationOptions 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) { this.hydrate(data as Data); } diff --git a/src/pages/RevoltApp.tsx b/src/pages/RevoltApp.tsx index 69ccf6f5..c04917ab 100644 --- a/src/pages/RevoltApp.tsx +++ b/src/pages/RevoltApp.tsx @@ -8,7 +8,6 @@ import ContextMenus from "../lib/ContextMenus"; import { isTouchscreenDevice } from "../lib/isTouchscreenDevice"; import Popovers from "../context/intermediate/Popovers"; -import Notifications from "../context/revoltjs/Notifications"; import { Titlebar } from "../components/native/Titlebar"; import BottomNavigation from "../components/navigation/BottomNavigation"; @@ -227,7 +226,6 @@ export default function App() { - From 8501e331039ad618135278b12fa6ff4c90e885b5 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 29 Jun 2022 17:33:23 +0100 Subject: [PATCH 14/17] chore(refactor): move `RequiresOnline` into controllers --- src/components/common/messaging/attachments/TextFile.tsx | 3 +-- .../revoltjs => controllers/client/jsx}/RequiresOnline.tsx | 2 +- src/pages/channels/messaging/MessageArea.tsx | 2 +- src/pages/channels/messaging/MessageRenderer.tsx | 3 +-- src/pages/invite/Invite.tsx | 2 +- src/pages/settings/ServerSettings.tsx | 2 +- src/pages/settings/Settings.tsx | 2 +- 7 files changed, 7 insertions(+), 9 deletions(-) rename src/{context/revoltjs => controllers/client/jsx}/RequiresOnline.tsx (93%) diff --git a/src/components/common/messaging/attachments/TextFile.tsx b/src/components/common/messaging/attachments/TextFile.tsx index 1dd2940b..ff417f89 100644 --- a/src/components/common/messaging/attachments/TextFile.tsx +++ b/src/components/common/messaging/attachments/TextFile.tsx @@ -7,9 +7,8 @@ import { useEffect, useState } from "preact/hooks"; import { Button, Preloader } from "@revoltchat/ui"; -import RequiresOnline from "../../../../context/revoltjs/RequiresOnline"; - import { useClient } from "../../../../controllers/client/ClientController"; +import RequiresOnline from "../../../../controllers/client/jsx/RequiresOnline"; interface Props { attachment: API.File; diff --git a/src/context/revoltjs/RequiresOnline.tsx b/src/controllers/client/jsx/RequiresOnline.tsx similarity index 93% rename from src/context/revoltjs/RequiresOnline.tsx rename to src/controllers/client/jsx/RequiresOnline.tsx index 1cc65038..d8ed18ec 100644 --- a/src/context/revoltjs/RequiresOnline.tsx +++ b/src/controllers/client/jsx/RequiresOnline.tsx @@ -5,7 +5,7 @@ import { Text } from "preact-i18n"; import { Preloader } from "@revoltchat/ui"; -import { useSession } from "../../controllers/client/ClientController"; +import { useSession } from "../ClientController"; interface Props { children: Children; diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index b74a9ad9..c35878d9 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -24,9 +24,9 @@ import { getRenderer } from "../../../lib/renderer/Singleton"; import { ScrollState } from "../../../lib/renderer/types"; import { IntermediateContext } from "../../../context/intermediate/Intermediate"; -import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; import { useSession } from "../../../controllers/client/ClientController"; +import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline"; import ConversationStart from "./ConversationStart"; import MessageRenderer from "./MessageRenderer"; diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index fa90cf0e..1523e9e7 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -17,11 +17,10 @@ import { ChannelRenderer } from "../../../lib/renderer/Singleton"; import { useApplicationState } from "../../../mobx/State"; -import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; - import Message from "../../../components/common/messaging/Message"; import { SystemMessage } from "../../../components/common/messaging/SystemMessage"; import { useClient } from "../../../controllers/client/ClientController"; +import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline"; import ConversationStart from "./ConversationStart"; import MessageEditor from "./MessageEditor"; diff --git a/src/pages/invite/Invite.tsx b/src/pages/invite/Invite.tsx index 1b28a9f0..d7307efb 100644 --- a/src/pages/invite/Invite.tsx +++ b/src/pages/invite/Invite.tsx @@ -12,7 +12,6 @@ import { TextReact } from "../../lib/i18n"; import { useApplicationState } from "../../mobx/State"; -import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import { takeError } from "../../context/revoltjs/util"; import ServerIcon from "../../components/common/ServerIcon"; @@ -21,6 +20,7 @@ import { useClient, useSession, } from "../../controllers/client/ClientController"; +import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; export default function Invite() { const history = useHistory(); diff --git a/src/pages/settings/ServerSettings.tsx b/src/pages/settings/ServerSettings.tsx index 5448250b..babd2755 100644 --- a/src/pages/settings/ServerSettings.tsx +++ b/src/pages/settings/ServerSettings.tsx @@ -16,10 +16,10 @@ import { Text } from "preact-i18n"; import { LineDivider } from "@revoltchat/ui"; import { useIntermediate } from "../../context/intermediate/Intermediate"; -import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import ButtonItem from "../../components/navigation/items/ButtonItem"; import { useClient } from "../../controllers/client/ClientController"; +import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; import { GenericSettings } from "./GenericSettings"; import { Bans } from "./server/Bans"; import { Categories } from "./server/Categories"; diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index a196553a..cc697ccc 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -34,7 +34,6 @@ import { LineDivider } from "@revoltchat/ui"; import { useApplicationState } from "../../mobx/State"; import { useIntermediate } from "../../context/intermediate/Intermediate"; -import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import UserIcon from "../../components/common/user/UserIcon"; import { Username } from "../../components/common/user/UserShort"; @@ -44,6 +43,7 @@ import { useClient, clientController, } from "../../controllers/client/ClientController"; +import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; import { modalController } from "../../controllers/modals/ModalController"; import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../revision"; import { APP_VERSION } from "../../version"; From 1664aaee1529edc777ca9a0659125d65b858cd07 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Thu, 30 Jun 2022 19:06:49 +0100 Subject: [PATCH 15/17] feat: add `ServerInfo`, port `ChannelInfo` --- external/lang | 2 +- src/components/common/ServerHeader.tsx | 12 ++++- src/components/markdown/Markdown.module.scss | 2 + src/context/intermediate/Popovers.tsx | 4 -- src/controllers/modals/ModalController.tsx | 13 +++++ .../modals/components/ChannelInfo.tsx | 29 +++++++++++ .../modals/components/ServerInfo.tsx | 48 +++++++++++++++++++ src/controllers/modals/types.ts | 10 +++- src/controllers/safety/index.ts | 16 +++++++ src/pages/channels/ChannelHeader.tsx | 11 ++--- 10 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 src/controllers/modals/components/ChannelInfo.tsx create mode 100644 src/controllers/modals/components/ServerInfo.tsx create mode 100644 src/controllers/safety/index.ts diff --git a/external/lang b/external/lang index 50838167..d4bc47b7 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit 50838167d7d253de9d08715e6a6070c3ddc9fcc2 +Subproject commit d4bc47b729c7e69ce97216469692b39f4cd1640e diff --git a/src/components/common/ServerHeader.tsx b/src/components/common/ServerHeader.tsx index 0c5b3f0e..69ff5a7b 100644 --- a/src/components/common/ServerHeader.tsx +++ b/src/components/common/ServerHeader.tsx @@ -9,6 +9,7 @@ import { Text } from "preact-i18n"; import { IconButton } from "@revoltchat/ui"; +import { modalController } from "../../controllers/modals/ModalController"; import Tooltip from "./Tooltip"; interface Props { @@ -60,6 +61,9 @@ const ServerBanner = styled.div>` overflow: hidden; text-overflow: ellipsis; flex-grow: 1; + + cursor: pointer; + color: var(--foreground); } } `; @@ -121,7 +125,13 @@ export default observer(({ server }: Props) => { ) : undefined} -
{server.name}
+ + modalController.push({ type: "server_info", server }) + }> + {server.name} + {server.havePermission("ManageServer") && ( diff --git a/src/components/markdown/Markdown.module.scss b/src/components/markdown/Markdown.module.scss index c107fcfa..cff232a1 100644 --- a/src/components/markdown/Markdown.module.scss +++ b/src/components/markdown/Markdown.module.scss @@ -1,4 +1,6 @@ .markdown { + user-select: text; + :global(.emoji) { object-fit: contain; diff --git a/src/context/intermediate/Popovers.tsx b/src/context/intermediate/Popovers.tsx index f5d8f347..0dc85eed 100644 --- a/src/context/intermediate/Popovers.tsx +++ b/src/context/intermediate/Popovers.tsx @@ -3,7 +3,6 @@ import { useContext } from "preact/hooks"; import { IntermediateContext, useIntermediate } from "./Intermediate"; import { SpecialInputModal } from "./modals/Input"; 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"; @@ -27,9 +26,6 @@ export default function Popovers() { return ; case "image_viewer": return ; - case "channel_info": - // @ts-expect-error someone figure this out :) - return ; case "create_bot": // @ts-expect-error someone figure this out :) return ; diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx index 8102cab8..3f78b7bc 100644 --- a/src/controllers/modals/ModalController.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -18,6 +18,7 @@ import { __thisIsAHack } from "../../context/intermediate/Intermediate"; // import { determineLink } from "../../lib/links"; import Changelog from "./components/Changelog"; +import ChannelInfo from "./components/ChannelInfo"; import Clipboard from "./components/Clipboard"; import Error from "./components/Error"; import LinkWarning from "./components/LinkWarning"; @@ -28,6 +29,7 @@ import ModifyAccount from "./components/ModifyAccount"; import OutOfDate from "./components/OutOfDate"; import PendingFriendRequests from "./components/PendingFriendRequests"; import ServerIdentity from "./components/ServerIdentity"; +import ServerInfo from "./components/ServerInfo"; import ShowToken from "./components/ShowToken"; import SignOutSessions from "./components/SignOutSessions"; import SignedOut from "./components/SignedOut"; @@ -54,6 +56,8 @@ class ModalController { isVisible: computed, }); + this.close = this.close.bind(this); + // Inject globally injectController("modal", this); } @@ -82,6 +86,13 @@ class ModalController { ); } + /** + * Close the top modal + */ + close() { + this.pop("close"); + } + /** * Remove the keyed modal from the stack */ @@ -208,6 +219,7 @@ class ModalControllerExtended extends ModalController { export const modalController = new ModalControllerExtended({ changelog: Changelog, + channel_info: ChannelInfo, clipboard: Clipboard, error: Error, link_warning: LinkWarning, @@ -218,6 +230,7 @@ export const modalController = new ModalControllerExtended({ out_of_date: OutOfDate, pending_friend_requests: PendingFriendRequests, server_identity: ServerIdentity, + server_info: ServerInfo, show_token: ShowToken, signed_out: SignedOut, sign_out_sessions: SignOutSessions, diff --git a/src/controllers/modals/components/ChannelInfo.tsx b/src/controllers/modals/components/ChannelInfo.tsx new file mode 100644 index 00000000..0e2149b0 --- /dev/null +++ b/src/controllers/modals/components/ChannelInfo.tsx @@ -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 ( + + +

{`#${channel.name}`}

+
+ + + + + }> + +
+ ); +} diff --git a/src/controllers/modals/components/ServerInfo.tsx b/src/controllers/modals/components/ServerInfo.tsx new file mode 100644 index 00000000..786026ad --- /dev/null +++ b/src/controllers/modals/components/ServerInfo.tsx @@ -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 ( + + +

{server.name}

+
+ + + + + } + actions={[ + { + onClick: () => + modalController.push({ + type: "server_identity", + member: server.member!, + }), + children: "Edit Identity", + palette: "primary", + }, + { + onClick: () => report(server), + children: , + palette: "error", + }, + ]}> + +
+ ); +} diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts index d95297ac..160673af 100644 --- a/src/controllers/modals/types.ts +++ b/src/controllers/modals/types.ts @@ -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 = { key?: string; @@ -72,6 +72,14 @@ export type Modal = { | { type: "signed_out"; } + | { + type: "channel_info"; + channel: Channel; + } + | { + type: "server_info"; + server: Server; + } ); export type ModalProps = Modal & { type: T } & { diff --git a/src/controllers/safety/index.ts b/src/controllers/safety/index.ts new file mode 100644 index 00000000..bfcb2471 --- /dev/null +++ b/src/controllers/safety/index.ts @@ -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!`, + )}`, + ); +} diff --git a/src/pages/channels/ChannelHeader.tsx b/src/pages/channels/ChannelHeader.tsx index 46860e4b..e2def382 100644 --- a/src/pages/channels/ChannelHeader.tsx +++ b/src/pages/channels/ChannelHeader.tsx @@ -1,19 +1,18 @@ import { At, Hash } from "@styled-icons/boxicons-regular"; import { Notepad, Group } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; -import { Channel } from "revolt.js"; -import { User } from "revolt.js"; +import { Channel, User } from "revolt.js"; import styled from "styled-components/macro"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; -import { useIntermediate } from "../../context/intermediate/Intermediate"; import { getChannelName } from "../../context/revoltjs/util"; import { useStatusColour } from "../../components/common/user/UserIcon"; import UserStatus from "../../components/common/user/UserStatus"; import Markdown from "../../components/markdown/Markdown"; import { PageHeader } from "../../components/ui/Header"; +import { modalController } from "../../controllers/modals/ModalController"; import HeaderActions from "./actions/HeaderActions"; export interface ChannelHeaderProps { @@ -65,8 +64,6 @@ const Info = styled.div` `; export default observer(({ channel }: ChannelHeaderProps) => { - const { openScreen } = useIntermediate(); - const name = getChannelName(channel); let icon, recipient: User | undefined; switch (channel.channel_type) { @@ -114,8 +111,8 @@ export default observer(({ channel }: ChannelHeaderProps) => { - openScreen({ - id: "channel_info", + modalController.push({ + type: "channel_info", channel, }) }> From 0d3f29515e3ec8f1aab179914aa6634d323b2ded Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Thu, 30 Jun 2022 19:34:04 +0100 Subject: [PATCH 16/17] feat: port `ImageViewer` --- .../messaging/attachments/ImageFile.tsx | 10 ++-- .../common/messaging/embed/Embed.tsx | 7 ++- .../common/messaging/embed/EmbedMedia.tsx | 8 +-- src/context/intermediate/Popovers.tsx | 3 - .../popovers/ChannelInfo.module.scss | 16 ------ .../intermediate/popovers/ChannelInfo.tsx | 41 -------------- .../popovers/ImageViewer.module.scss | 20 ------- .../intermediate/popovers/UserProfile.tsx | 7 ++- src/controllers/modals/ModalController.tsx | 2 + .../modals/components}/ImageViewer.tsx | 55 ++++++++++++------- src/controllers/modals/types.ts | 5 ++ 11 files changed, 58 insertions(+), 116 deletions(-) delete mode 100644 src/context/intermediate/popovers/ChannelInfo.module.scss delete mode 100644 src/context/intermediate/popovers/ChannelInfo.tsx delete mode 100644 src/context/intermediate/popovers/ImageViewer.module.scss rename src/{context/intermediate/popovers => controllers/modals/components}/ImageViewer.tsx (58%) diff --git a/src/components/common/messaging/attachments/ImageFile.tsx b/src/components/common/messaging/attachments/ImageFile.tsx index 76723e20..7c2364d2 100644 --- a/src/components/common/messaging/attachments/ImageFile.tsx +++ b/src/components/common/messaging/attachments/ImageFile.tsx @@ -2,11 +2,10 @@ import { API } from "revolt.js"; import styles from "./Attachment.module.scss"; import classNames from "classnames"; -import { useContext, useState } from "preact/hooks"; - -import { useIntermediate } from "../../../../context/intermediate/Intermediate"; +import { useState } from "preact/hooks"; import { useClient } from "../../../../controllers/client/ClientController"; +import { modalController } from "../../../../controllers/modals/ModalController"; enum ImageLoadingState { Loading, @@ -21,7 +20,6 @@ type Props = JSX.HTMLAttributes & { export default function ImageFile({ attachment, ...props }: Props) { const [loading, setLoading] = useState(ImageLoadingState.Loading); const client = useClient(); - const { openScreen } = useIntermediate(); const url = client.generateFileURL(attachment)!; return ( @@ -33,7 +31,9 @@ export default function ImageFile({ attachment, ...props }: Props) { className={classNames(styles.image, { [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")} onLoad={() => setLoading(ImageLoadingState.Loaded)} onError={() => setLoading(ImageLoadingState.Error)} diff --git a/src/components/common/messaging/embed/Embed.tsx b/src/components/common/messaging/embed/Embed.tsx index 306f6441..822f30b2 100644 --- a/src/components/common/messaging/embed/Embed.tsx +++ b/src/components/common/messaging/embed/Embed.tsx @@ -7,6 +7,7 @@ import { useContext } from "preact/hooks"; import { useIntermediate } from "../../../../context/intermediate/Intermediate"; import { useClient } from "../../../../controllers/client/ClientController"; +import { modalController } from "../../../../controllers/modals/ModalController"; import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea"; import Markdown from "../../../markdown/Markdown"; import Attachment from "../attachments/Attachment"; @@ -24,7 +25,7 @@ const MAX_PREVIEW_SIZE = 150; export default function Embed({ embed }: Props) { const client = useClient(); - const { openScreen, openLink } = useIntermediate(); + const { openLink } = useIntermediate(); const maxWidth = Math.min( useContext(MessageAreaWidthContext) - CONTAINER_PADDING, MAX_EMBED_WIDTH, @@ -191,7 +192,9 @@ export default function Embed({ embed }: Props) { type="text/html" frameBorder="0" loading="lazy" - onClick={() => openScreen({ id: "image_viewer", embed })} + onClick={() => + modalController.push({ type: "image_viewer", embed }) + } onMouseDown={(ev) => ev.button === 1 && openLink(embed.url)} /> ); diff --git a/src/components/common/messaging/embed/EmbedMedia.tsx b/src/components/common/messaging/embed/EmbedMedia.tsx index a719f0e3..201b3d84 100644 --- a/src/components/common/messaging/embed/EmbedMedia.tsx +++ b/src/components/common/messaging/embed/EmbedMedia.tsx @@ -3,9 +3,8 @@ import { API } from "revolt.js"; import styles from "./Embed.module.scss"; -import { useIntermediate } from "../../../../context/intermediate/Intermediate"; - import { useClient } from "../../../../controllers/client/ClientController"; +import { modalController } from "../../../../controllers/modals/ModalController"; interface Props { embed: API.Embed; @@ -15,7 +14,6 @@ interface Props { export default function EmbedMedia({ embed, width, height }: Props) { if (embed.type !== "Website") return null; - const { openScreen } = useIntermediate(); const client = useClient(); switch (embed.special?.type) { @@ -118,8 +116,8 @@ export default function EmbedMedia({ embed, width, height }: Props) { loading="lazy" style={{ width, height }} onClick={() => - openScreen({ - id: "image_viewer", + modalController.push({ + type: "image_viewer", embed: embed.image!, }) } diff --git a/src/context/intermediate/Popovers.tsx b/src/context/intermediate/Popovers.tsx index 0dc85eed..20789cfd 100644 --- a/src/context/intermediate/Popovers.tsx +++ b/src/context/intermediate/Popovers.tsx @@ -4,7 +4,6 @@ import { IntermediateContext, useIntermediate } from "./Intermediate"; import { SpecialInputModal } from "./modals/Input"; import { SpecialPromptModal } from "./modals/Prompt"; import { CreateBotModal } from "./popovers/CreateBot"; -import { ImageViewer } from "./popovers/ImageViewer"; import { UserPicker } from "./popovers/UserPicker"; import { UserProfile } from "./popovers/UserProfile"; @@ -24,8 +23,6 @@ export default function Popovers() { case "user_picker": // @ts-expect-error someone figure this out :) return ; - case "image_viewer": - return ; case "create_bot": // @ts-expect-error someone figure this out :) return ; diff --git a/src/context/intermediate/popovers/ChannelInfo.module.scss b/src/context/intermediate/popovers/ChannelInfo.module.scss deleted file mode 100644 index ff37d169..00000000 --- a/src/context/intermediate/popovers/ChannelInfo.module.scss +++ /dev/null @@ -1,16 +0,0 @@ -.info { - .header { - display: flex; - align-items: center; - flex-direction: row; - - h1 { - margin: 0; - flex-grow: 1; - } - - div { - cursor: pointer; - } - } -} diff --git a/src/context/intermediate/popovers/ChannelInfo.tsx b/src/context/intermediate/popovers/ChannelInfo.tsx deleted file mode 100644 index d2a37f42..00000000 --- a/src/context/intermediate/popovers/ChannelInfo.tsx +++ /dev/null @@ -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 ( - -
-
-

{getChannelName(channel, true)}

-
- -
-
-

- -

-
-
- ); -}); diff --git a/src/context/intermediate/popovers/ImageViewer.module.scss b/src/context/intermediate/popovers/ImageViewer.module.scss deleted file mode 100644 index a450d0a7..00000000 --- a/src/context/intermediate/popovers/ImageViewer.module.scss +++ /dev/null @@ -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; - } -} diff --git a/src/context/intermediate/popovers/UserProfile.tsx b/src/context/intermediate/popovers/UserProfile.tsx index 5a4eb4c0..764a3b34 100644 --- a/src/context/intermediate/popovers/UserProfile.tsx +++ b/src/context/intermediate/popovers/UserProfile.tsx @@ -13,7 +13,7 @@ import { UserPermission, API } from "revolt.js"; import styles from "./UserProfile.module.scss"; import { Localizer, Text } from "preact-i18n"; -import { useContext, useEffect, useLayoutEffect, useState } from "preact/hooks"; +import { useEffect, useLayoutEffect, useState } from "preact/hooks"; import { Button, @@ -35,6 +35,7 @@ import { Username } from "../../../components/common/user/UserShort"; import UserStatus from "../../../components/common/user/UserStatus"; import Markdown from "../../../components/markdown/Markdown"; import { useSession } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; import { useIntermediate } from "../Intermediate"; interface Props { @@ -159,8 +160,8 @@ export const UserProfile = observer( hover={typeof user.avatar !== "undefined"} onClick={() => user.avatar && - openScreen({ - id: "image_viewer", + modalController.push({ + type: "image_viewer", attachment: user.avatar, }) } diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx index 3f78b7bc..cc6e7f9a 100644 --- a/src/controllers/modals/ModalController.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -21,6 +21,7 @@ import Changelog from "./components/Changelog"; import ChannelInfo from "./components/ChannelInfo"; import Clipboard from "./components/Clipboard"; import Error from "./components/Error"; +import ImageViewer from "./components/ImageViewer"; import LinkWarning from "./components/LinkWarning"; import MFAEnableTOTP from "./components/MFAEnableTOTP"; import MFAFlow from "./components/MFAFlow"; @@ -222,6 +223,7 @@ export const modalController = new ModalControllerExtended({ channel_info: ChannelInfo, clipboard: Clipboard, error: Error, + image_viewer: ImageViewer, link_warning: LinkWarning, mfa_flow: MFAFlow, mfa_recovery: MFARecovery, diff --git a/src/context/intermediate/popovers/ImageViewer.tsx b/src/controllers/modals/components/ImageViewer.tsx similarity index 58% rename from src/context/intermediate/popovers/ImageViewer.tsx rename to src/controllers/modals/components/ImageViewer.tsx index 12a17f3c..526ec9c4 100644 --- a/src/context/intermediate/popovers/ImageViewer.tsx +++ b/src/controllers/modals/components/ImageViewer.tsx @@ -1,23 +1,40 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { API } from "revolt.js"; - -import styles from "./ImageViewer.module.scss"; +import styled from "styled-components"; import { Modal } from "@revoltchat/ui"; import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions"; import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions"; -import { useClient } from "../../../controllers/client/ClientController"; +import { useClient } from "../../client/ClientController"; +import { ModalProps } from "../types"; -interface Props { - onClose: () => void; - embed?: API.Image; - attachment?: API.File; -} +const Viewer = styled.div` + display: flex; + overflow: hidden; + 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") { console.warn( `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; } - const client = useClient(); - return ( - -
+ + {attachment && ( <> @@ -54,7 +67,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) { )} -
+
); } diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts index 160673af..e2e3b854 100644 --- a/src/controllers/modals/types.ts +++ b/src/controllers/modals/types.ts @@ -80,6 +80,11 @@ export type Modal = { type: "server_info"; server: Server; } + | { + type: "image_viewer"; + embed?: API.Image; + attachment?: API.File; + } ); export type ModalProps = Modal & { type: T } & { From 401b2d4990ae3b2d3b17fd266d916048bf0b1346 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Thu, 30 Jun 2022 20:39:00 +0100 Subject: [PATCH 17/17] feat: port `UserProfile`, `Onboarding`, `CreateBot` to legacy --- src/components/common/messaging/Message.tsx | 7 ++- src/components/common/user/UserShort.tsx | 7 +-- .../navigation/right/MemberList.tsx | 40 ++++--------- src/context/intermediate/Intermediate.tsx | 2 +- src/context/intermediate/Modals.tsx | 3 - src/context/intermediate/Popovers.tsx | 12 ---- src/controllers/client/Session.tsx | 6 +- src/controllers/modals/ModalController.tsx | 11 +++- .../modals/components}/UserPicker.tsx | 39 +++++++----- .../modals/components/legacy}/CreateBot.tsx | 27 +++++---- .../components/legacy}/Onboarding.module.scss | 9 ++- .../modals/components/legacy}/Onboarding.tsx | 18 +++--- .../legacy}/UserProfile.module.scss | 0 .../modals/components/legacy}/UserProfile.tsx | 60 +++++++++---------- src/controllers/modals/types.ts | 22 +++++++ src/lib/ContextMenus.tsx | 5 +- src/pages/channels/actions/HeaderActions.tsx | 5 +- src/pages/friends/Friend.tsx | 8 ++- src/pages/settings/panes/Profile.tsx | 3 +- 19 files changed, 155 insertions(+), 129 deletions(-) rename src/{context/intermediate/popovers => controllers/modals/components}/UserPicker.tsx (67%) rename src/{context/intermediate/popovers => controllers/modals/components/legacy}/CreateBot.tsx (79%) rename src/{context/intermediate/modals => controllers/modals/components/legacy}/Onboarding.module.scss (87%) rename src/{context/intermediate/modals => controllers/modals/components/legacy}/Onboarding.tsx (87%) rename src/{context/intermediate/popovers => controllers/modals/components/legacy}/UserProfile.module.scss (100%) rename src/{context/intermediate/popovers => controllers/modals/components/legacy}/UserProfile.tsx (92%) diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index 3e15f001..8f684141 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -15,7 +15,7 @@ import { QueuedMessage } from "../../../mobx/stores/MessageQueue"; import { I18nError } from "../../../context/Locale"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; -import { useClient } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; import Markdown from "../../markdown/Markdown"; import UserIcon from "../user/UserIcon"; import { Username } from "../user/UserShort"; @@ -70,7 +70,10 @@ const Message = observer( : undefined; const openProfile = () => - openScreen({ id: "profile", user_id: message.author_id }); + modalController.push({ + type: "user_profile", + user_id: message.author_id, + }); const handleUserClick = (e: MouseEvent) => { if (e.shiftKey && user?._id) { diff --git a/src/components/common/user/UserShort.tsx b/src/components/common/user/UserShort.tsx index 5ac03d19..bbe3670a 100644 --- a/src/components/common/user/UserShort.tsx +++ b/src/components/common/user/UserShort.tsx @@ -8,9 +8,8 @@ import { Text } from "preact-i18n"; import { internalEmit } from "../../../lib/eventEmitter"; -import { useIntermediate } from "../../../context/intermediate/Intermediate"; - import { useClient } from "../../../controllers/client/ClientController"; +import { modalController } from "../../../controllers/modals/ModalController"; import UserIcon from "./UserIcon"; const BotBadge = styled.div` @@ -125,9 +124,9 @@ export default function UserShort({ masquerade?: API.Masquerade; showServerIdentity?: boolean; }) { - const { openScreen } = useIntermediate(); const openProfile = () => - user && openScreen({ id: "profile", user_id: user._id }); + user && + modalController.push({ type: "user_profile", user_id: user._id }); const handleUserClick = (e: MouseEvent) => { if (e.shiftKey && user?._id) { diff --git a/src/components/navigation/right/MemberList.tsx b/src/components/navigation/right/MemberList.tsx index 8b9495ba..3913c20b 100644 --- a/src/components/navigation/right/MemberList.tsx +++ b/src/components/navigation/right/MemberList.tsx @@ -8,11 +8,7 @@ import { memo } from "preact/compat"; import { internalEmit } from "../../../lib/eventEmitter"; -import { - Screen, - useIntermediate, -} from "../../../context/intermediate/Intermediate"; - +import { modalController } from "../../../controllers/modals/ModalController"; import { UserButton } from "../items/ButtonItem"; export type MemberListGroup = { @@ -55,15 +51,7 @@ const NoOomfie = styled.div` `; const ItemContent = memo( - ({ - item, - context, - openScreen, - }: { - item: User; - context: Channel; - openScreen: (screen: Screen) => void; - }) => ( + ({ item, context }: { item: User; context: Channel }) => ( `, "mention", ); - } else - [ - openScreen({ - id: "profile", - user_id: item._id, - }), - ]; + } else { + modalController.push({ + type: "user_profile", + user_id: item._id, + }); + } }} /> ), @@ -96,8 +83,6 @@ export default function MemberList({ entries: MemberListGroup[]; context: Channel; }) { - const { openScreen } = useIntermediate(); - return ( x.users.length)} @@ -137,7 +122,8 @@ export default function MemberList({ server, see issue{" "} + target="_blank" + rel="noreferrer"> #128 {" "} for when this will be resolved. @@ -158,11 +144,7 @@ export default function MemberList({ return (
- +
); }} diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 32df0c1b..e67ca0ae 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -159,7 +159,7 @@ export default function Intermediate(props: Props) { useEffect(() => { 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 subs = [ diff --git a/src/context/intermediate/Modals.tsx b/src/context/intermediate/Modals.tsx index 4f59c974..48e18245 100644 --- a/src/context/intermediate/Modals.tsx +++ b/src/context/intermediate/Modals.tsx @@ -1,7 +1,6 @@ //import { isModalClosing } from "../../components/ui/Modal"; import { Screen } from "./Intermediate"; import { InputModal } from "./modals/Input"; -import { OnboardingModal } from "./modals/Onboarding"; import { PromptModal } from "./modals/Prompt"; export interface Props { @@ -20,8 +19,6 @@ export default function Modals({ screen, openScreen }: Props) { return ; case "_input": return ; - case "onboarding": - return ; } return null; diff --git a/src/context/intermediate/Popovers.tsx b/src/context/intermediate/Popovers.tsx index 20789cfd..08386727 100644 --- a/src/context/intermediate/Popovers.tsx +++ b/src/context/intermediate/Popovers.tsx @@ -3,9 +3,6 @@ import { useContext } from "preact/hooks"; import { IntermediateContext, useIntermediate } from "./Intermediate"; import { SpecialInputModal } from "./modals/Input"; import { SpecialPromptModal } from "./modals/Prompt"; -import { CreateBotModal } from "./popovers/CreateBot"; -import { UserPicker } from "./popovers/UserPicker"; -import { UserProfile } from "./popovers/UserProfile"; export default function Popovers() { const { screen } = useContext(IntermediateContext); @@ -17,15 +14,6 @@ export default function Popovers() { //: internalEmit("Modal", "close"); switch (screen.id) { - case "profile": - // @ts-expect-error someone figure this out :) - return ; - case "user_picker": - // @ts-expect-error someone figure this out :) - return ; - case "create_bot": - // @ts-expect-error someone figure this out :) - return ; case "special_prompt": // @ts-expect-error someone figure this out :) return ; diff --git a/src/controllers/client/Session.tsx b/src/controllers/client/Session.tsx index 11f80d71..442b65d0 100644 --- a/src/controllers/client/Session.tsx +++ b/src/controllers/client/Session.tsx @@ -5,6 +5,8 @@ import { state } from "../../mobx/State"; import { __thisIsAHack } from "../../context/intermediate/Intermediate"; +import { modalController } from "../modals/ModalController"; + /** * Current lifecycle state */ @@ -189,8 +191,8 @@ export default class Session { ); if (onboarding) { - __thisIsAHack({ - id: "onboarding", + modalController.push({ + type: "onboarding", callback: async (username: string) => this.client!.completeOnboarding( { username }, diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx index cc6e7f9a..a8169d0e 100644 --- a/src/controllers/modals/ModalController.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -16,7 +16,6 @@ import { getApplicationState } from "../../mobx/State"; import { history } from "../../context/history"; import { __thisIsAHack } from "../../context/intermediate/Intermediate"; -// import { determineLink } from "../../lib/links"; import Changelog from "./components/Changelog"; import ChannelInfo from "./components/ChannelInfo"; import Clipboard from "./components/Clipboard"; @@ -34,6 +33,10 @@ import ServerInfo from "./components/ServerInfo"; import ShowToken from "./components/ShowToken"; import SignOutSessions from "./components/SignOutSessions"; 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"; type Components = Record>; @@ -191,7 +194,7 @@ class ModalControllerExtended extends ModalController { switch (link.type) { case "profile": { - __thisIsAHack({ id: "profile", user_id: link.id }); + this.push({ type: "user_profile", user_id: link.id }); break; } case "navigate": { @@ -222,6 +225,7 @@ export const modalController = new ModalControllerExtended({ changelog: Changelog, channel_info: ChannelInfo, clipboard: Clipboard, + create_bot: CreateBotModal, error: Error, image_viewer: ImageViewer, link_warning: LinkWarning, @@ -229,6 +233,7 @@ export const modalController = new ModalControllerExtended({ mfa_recovery: MFARecovery, mfa_enable_totp: MFAEnableTOTP, modify_account: ModifyAccount, + onboarding: OnboardingModal, out_of_date: OutOfDate, pending_friend_requests: PendingFriendRequests, server_identity: ServerIdentity, @@ -236,4 +241,6 @@ export const modalController = new ModalControllerExtended({ show_token: ShowToken, signed_out: SignedOut, sign_out_sessions: SignOutSessions, + user_picker: UserPicker, + user_profile: UserProfile, }); diff --git a/src/context/intermediate/popovers/UserPicker.tsx b/src/controllers/modals/components/UserPicker.tsx similarity index 67% rename from src/context/intermediate/popovers/UserPicker.tsx rename to src/controllers/modals/components/UserPicker.tsx index b152e8d5..b4cc6633 100644 --- a/src/context/intermediate/popovers/UserPicker.tsx +++ b/src/controllers/modals/components/UserPicker.tsx @@ -1,41 +1,50 @@ -import styles from "./UserPicker.module.scss"; +import styled from "styled-components"; + import { Text } from "preact-i18n"; -import { useState } from "preact/hooks"; +import { useMemo, useState } from "preact/hooks"; import { Modal } from "@revoltchat/ui"; import UserCheckbox from "../../../components/common/user/UserCheckbox"; -import { useClient } from "../../../controllers/client/ClientController"; +import { useClient } from "../../client/ClientController"; +import { ModalProps } from "../types"; -interface Props { - omit?: string[]; - onClose: () => void; - callback: (users: string[]) => Promise; -} +const List = styled.div` + max-width: 100%; + max-height: 360px; + overflow-y: scroll; +`; -export function UserPicker(props: Props) { +export function UserPicker({ + callback, + omit, + ...props +}: ModalProps<"user_picker">) { const [selected, setSelected] = useState([]); - const omit = [...(props.omit || []), "00000000000000000000000000"]; + const omitted = useMemo( + () => new Set([...(omit || []), "00000000000000000000000000"]), + [omit], + ); const client = useClient(); return ( } - onClose={props.onClose} actions={[ { children: , - onClick: () => props.callback(selected).then(() => true), + onClick: () => callback(selected).then(() => true), }, ]}> -
+ {[...client.users.values()] .filter( (x) => x && x.relationship === "Friend" && - !omit.includes(x._id), + !omitted.has(x._id), ) .map((x) => ( ))} -
+
); } diff --git a/src/context/intermediate/popovers/CreateBot.tsx b/src/controllers/modals/components/legacy/CreateBot.tsx similarity index 79% rename from src/context/intermediate/popovers/CreateBot.tsx rename to src/controllers/modals/components/legacy/CreateBot.tsx index 142996cd..b880ef5a 100644 --- a/src/context/intermediate/popovers/CreateBot.tsx +++ b/src/controllers/modals/components/legacy/CreateBot.tsx @@ -6,21 +6,24 @@ 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 { takeError } from "../../revoltjs/util"; +import { noopTrue } from "../../../../lib/js"; -interface Props { - onClose: () => void; - onCreate: (bot: API.Bot) => void; -} +import { I18nError } from "../../../../context/Locale"; +import { takeError } from "../../../../context/revoltjs/util"; + +import FormField from "../../../../pages/login/FormField"; +import { useClient } from "../../../client/ClientController"; +import { modalController } from "../../ModalController"; +import { ModalProps } from "../../types"; interface FormInputs { name: string; } -export function CreateBotModal({ onClose, onCreate }: Props) { +export function CreateBotModal({ + onCreate, + ...props +}: ModalProps<"create_bot">) { const client = useClient(); const { handleSubmit, register, errors } = useForm(); const [error, setError] = useState(undefined); @@ -29,7 +32,7 @@ export function CreateBotModal({ onClose, onCreate }: Props) { try { const { bot } = await client.bots.create({ name }); onCreate(bot); - onClose(); + modalController.close(); } catch (err) { setError(takeError(err)); } @@ -37,7 +40,7 @@ export function CreateBotModal({ onClose, onCreate }: Props) { return ( } actions={[ { @@ -51,7 +54,7 @@ export function CreateBotModal({ onClose, onCreate }: Props) { }, { palette: "plain", - onClick: onClose, + onClick: noopTrue, children: , }, ]}> diff --git a/src/context/intermediate/modals/Onboarding.module.scss b/src/controllers/modals/components/legacy/Onboarding.module.scss similarity index 87% rename from src/context/intermediate/modals/Onboarding.module.scss rename to src/controllers/modals/components/legacy/Onboarding.module.scss index 887b4224..42ebfbdd 100644 --- a/src/context/intermediate/modals/Onboarding.module.scss +++ b/src/controllers/modals/components/legacy/Onboarding.module.scss @@ -1,5 +1,12 @@ .onboarding { - height: 100vh; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: var(--background); + display: flex; align-items: center; flex-direction: column; diff --git a/src/context/intermediate/modals/Onboarding.tsx b/src/controllers/modals/components/legacy/Onboarding.tsx similarity index 87% rename from src/context/intermediate/modals/Onboarding.tsx rename to src/controllers/modals/components/legacy/Onboarding.tsx index af21e930..53cc681f 100644 --- a/src/context/intermediate/modals/Onboarding.tsx +++ b/src/controllers/modals/components/legacy/Onboarding.tsx @@ -6,21 +6,21 @@ import { useState } from "preact/hooks"; import { Button, Preloader } from "@revoltchat/ui"; +import { takeError } from "../../../../context/revoltjs/util"; + import wideSVG from "/assets/wide.svg"; -import FormField from "../../../pages/login/FormField"; -import { takeError } from "../../revoltjs/util"; - -interface Props { - onClose: () => void; - callback: (username: string, loginAfterSuccess?: true) => Promise; -} +import FormField from "../../../../pages/login/FormField"; +import { ModalProps } from "../../types"; interface FormInputs { username: string; } -export function OnboardingModal({ onClose, callback }: Props) { +export function OnboardingModal({ + callback, + ...props +}: ModalProps<"onboarding">) { const { handleSubmit, register } = useForm(); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); @@ -28,7 +28,7 @@ export function OnboardingModal({ onClose, callback }: Props) { const onSubmit: SubmitHandler = ({ username }) => { setLoading(true); callback(username, true) - .then(() => onClose()) + .then(() => props.onClose()) .catch((err: unknown) => { setError(takeError(err)); setLoading(false); diff --git a/src/context/intermediate/popovers/UserProfile.module.scss b/src/controllers/modals/components/legacy/UserProfile.module.scss similarity index 100% rename from src/context/intermediate/popovers/UserProfile.module.scss rename to src/controllers/modals/components/legacy/UserProfile.module.scss diff --git a/src/context/intermediate/popovers/UserProfile.tsx b/src/controllers/modals/components/legacy/UserProfile.tsx similarity index 92% rename from src/context/intermediate/popovers/UserProfile.tsx rename to src/controllers/modals/components/legacy/UserProfile.tsx index 764a3b34..a7cc7f8e 100644 --- a/src/context/intermediate/popovers/UserProfile.tsx +++ b/src/controllers/modals/components/legacy/UserProfile.tsx @@ -24,31 +24,27 @@ import { Preloader, } from "@revoltchat/ui"; -import { noop } from "../../../lib/js"; +import { noop } from "../../../../lib/js"; -import ChannelIcon from "../../../components/common/ChannelIcon"; -import ServerIcon from "../../../components/common/ServerIcon"; -import Tooltip from "../../../components/common/Tooltip"; -import UserBadges from "../../../components/common/user/UserBadges"; -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 { useSession } from "../../../controllers/client/ClientController"; -import { modalController } from "../../../controllers/modals/ModalController"; -import { useIntermediate } from "../Intermediate"; - -interface Props { - user_id: string; - dummy?: boolean; - onClose?: () => void; - dummyProfile?: API.UserProfile; -} +import ChannelIcon from "../../../../components/common/ChannelIcon"; +import ServerIcon from "../../../../components/common/ServerIcon"; +import Tooltip from "../../../../components/common/Tooltip"; +import UserBadges from "../../../../components/common/user/UserBadges"; +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 { useSession } from "../../../../controllers/client/ClientController"; +import { modalController } from "../../../../controllers/modals/ModalController"; +import { ModalProps } from "../../types"; 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< undefined | null | API.UserProfile >(undefined); @@ -66,7 +62,7 @@ export const UserProfile = observer( const user = client.users.get(user_id); if (!user) { - if (onClose) useEffect(onClose, []); + if (props.onClose) useEffect(props.onClose, []); return null; } @@ -171,7 +167,7 @@ export const UserProfile = observer( - writeClipboard(user.username) + modalController.writeText(user.username) }> @{user.username} @@ -187,7 +183,7 @@ export const UserProfile = observer( @@ -200,7 +196,7 @@ export const UserProfile = observer( }> { - onClose?.(); + props.onClose?.(); history.push(`/open/${user_id}`); }}> @@ -211,7 +207,7 @@ export const UserProfile = observer( {user.relationship === "User" && !dummy && ( { - onClose?.(); + props.onClose?.(); history.push(`/settings/profile`); }}> @@ -291,8 +287,8 @@ export const UserProfile = observer(
user.bot && - openScreen({ - id: "profile", + modalController.push({ + type: "user_profile", user_id: user.bot.owner, }) } @@ -355,8 +351,8 @@ export const UserProfile = observer( x && (
- openScreen({ - id: "profile", + modalController.push({ + type: "user_profile", user_id: x._id, }) } @@ -436,7 +432,7 @@ export const UserProfile = observer( return ( diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts index e2e3b854..2471b82c 100644 --- a/src/controllers/modals/types.ts +++ b/src/controllers/modals/types.ts @@ -85,6 +85,28 @@ export type Modal = { embed?: API.Image; attachment?: API.File; } + | { + type: "user_picker"; + omit?: string[]; + callback: (users: string[]) => Promise; + } + | { + 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; + } ); export type ModalProps = Modal & { type: T } & { diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index ed96762c..6c1d1ddc 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -314,7 +314,10 @@ export default function ContextMenus() { break; case "view_profile": - openScreen({ id: "profile", user_id: data.user._id }); + modalController.push({ + type: "user_profile", + user_id: data.user._id, + }); break; case "message_user": diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx index c3bddff1..04cbe198 100644 --- a/src/pages/channels/actions/HeaderActions.tsx +++ b/src/pages/channels/actions/HeaderActions.tsx @@ -24,6 +24,7 @@ import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import UpdateIndicator from "../../../components/common/UpdateIndicator"; +import { modalController } from "../../../controllers/modals/ModalController"; import { ChannelHeaderProps } from "../ChannelHeader"; const Container = styled.div` @@ -114,8 +115,8 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) { <> - openScreen({ - id: "user_picker", + modalController.push({ + type: "user_picker", omit: channel.recipient_ids!, callback: async (users) => { for (const user of users) { diff --git a/src/pages/friends/Friend.tsx b/src/pages/friends/Friend.tsx index 182ffdea..f93d3531 100644 --- a/src/pages/friends/Friend.tsx +++ b/src/pages/friends/Friend.tsx @@ -18,6 +18,7 @@ import { useIntermediate } from "../../context/intermediate/Intermediate"; import UserIcon from "../../components/common/user/UserIcon"; import UserStatus from "../../components/common/user/UserStatus"; +import { modalController } from "../../controllers/modals/ModalController"; interface Props { user: User; @@ -128,7 +129,12 @@ export const Friend = observer(({ user }: Props) => { return (
openScreen({ id: "profile", user_id: user._id })} + onClick={() => + modalController.push({ + type: "user_profile", + user_id: user._id, + }) + } {...useTriggerEvents("Menu", { user: user._id, })}> diff --git a/src/pages/settings/panes/Profile.tsx b/src/pages/settings/panes/Profile.tsx index d9875090..c0c9e0c3 100644 --- a/src/pages/settings/panes/Profile.tsx +++ b/src/pages/settings/panes/Profile.tsx @@ -12,13 +12,13 @@ import { Button, LineDivider, Tip } from "@revoltchat/ui"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import { useTranslation } from "../../../lib/i18n"; -import { UserProfile } from "../../../context/intermediate/popovers/UserProfile"; import { FileUploader } from "../../../context/revoltjs/FileUploads"; import AutoComplete, { useAutoComplete, } from "../../../components/common/AutoComplete"; import { useSession } from "../../../controllers/client/ClientController"; +import { UserProfile } from "../../../controllers/modals/components/legacy/UserProfile"; export const Profile = observer(() => { const translate = useTranslation(); @@ -75,6 +75,7 @@ export const Profile = observer(() => { user_id={client.user!._id} dummy={true} dummyProfile={profile} + {...({} as any)} />
{/*

Badges