From cc0e45526cb82de8b41121c093ef6e180d4c9d91 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 23 Dec 2021 21:43:11 +0000 Subject: [PATCH] feat(mobx): refactor and remove (react-)redux --- .gitignore | 2 + .vscode/launch.json | 17 +++ package.json | 4 +- src/components/common/AgeGate.tsx | 28 ++--- src/components/common/messaging/Message.tsx | 2 +- .../common/messaging/MessageBox.tsx | 4 +- .../common/messaging/bars/ReplyBar.tsx | 16 +-- .../common/messaging/embed/EmbedInvite.tsx | 29 ++--- src/components/navigation/LeftSidebar.tsx | 8 +- .../navigation/left/ServerSidebar.tsx | 1 - src/components/navigation/left/common.ts | 79 ------------ src/components/ui/Modal.tsx | 2 +- src/context/Theme.tsx | 23 ---- src/context/index.tsx | 16 +-- .../modals/ExternalLinkPrompt.tsx | 1 - src/context/revoltjs/StateMonitor.tsx | 21 +--- src/context/revoltjs/SyncManager.tsx | 19 --- src/context/revoltjs/events.ts | 24 ---- src/lib/ContextMenus.tsx | 40 ++---- src/mobx/State.ts | 22 +++- src/mobx/implementation notes | 25 ---- src/mobx/legacy/redux.ts | 36 +++++- src/mobx/stores/Layout.ts | 14 +++ src/mobx/stores/Settings.ts | 8 +- src/mobx/stores/Sync.ts | 22 ++++ src/pages/channels/Channel.tsx | 59 ++------- src/pages/channels/ChannelHeader.tsx | 22 +++- src/pages/channels/actions/HeaderActions.tsx | 11 +- .../channels/messaging/MessageRenderer.tsx | 3 - src/pages/home/Home.tsx | 26 +--- src/pages/invite/Invite.tsx | 10 +- src/pages/settings/panes/Audio.tsx | 8 +- src/pages/settings/panes/Languages.tsx | 5 - src/pages/settings/panes/Notifications.tsx | 31 ++--- src/pages/settings/panes/Sync.tsx | 32 ++--- src/pages/settings/panes/ThemeShop.tsx | 3 +- src/redux/State.tsx | 49 -------- src/redux/connector.tsx | 16 --- src/redux/index.ts | 94 -------------- src/redux/reducers/auth.ts | 49 -------- src/redux/reducers/drafts.ts | 35 ------ src/redux/reducers/experiments.ts | 67 ---------- src/redux/reducers/index.ts | 51 -------- src/redux/reducers/last_opened.ts | 32 ----- src/redux/reducers/locale.ts | 56 --------- src/redux/reducers/notifications.ts | 83 ------------- src/redux/reducers/queue.ts | 115 ------------------ src/redux/reducers/section_toggle.ts | 40 ------ src/redux/reducers/server_config.ts | 20 --- src/redux/reducers/settings.ts | 112 ----------------- src/redux/reducers/sync.ts | 94 -------------- src/redux/reducers/themes.ts | 33 ----- src/redux/reducers/trusted_links.ts | 37 ------ src/redux/reducers/unreads.ts | 77 ------------ yarn.lock | 38 ++---- 55 files changed, 249 insertions(+), 1522 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 src/components/navigation/left/common.ts delete mode 100644 src/mobx/implementation notes delete mode 100644 src/redux/State.tsx delete mode 100644 src/redux/connector.tsx delete mode 100644 src/redux/index.ts delete mode 100644 src/redux/reducers/auth.ts delete mode 100644 src/redux/reducers/drafts.ts delete mode 100644 src/redux/reducers/experiments.ts delete mode 100644 src/redux/reducers/index.ts delete mode 100644 src/redux/reducers/last_opened.ts delete mode 100644 src/redux/reducers/locale.ts delete mode 100644 src/redux/reducers/notifications.ts delete mode 100644 src/redux/reducers/queue.ts delete mode 100644 src/redux/reducers/section_toggle.ts delete mode 100644 src/redux/reducers/server_config.ts delete mode 100644 src/redux/reducers/settings.ts delete mode 100644 src/redux/reducers/sync.ts delete mode 100644 src/redux/reducers/themes.ts delete mode 100644 src/redux/reducers/trusted_links.ts delete mode 100644 src/redux/reducers/unreads.ts diff --git a/.gitignore b/.gitignore index 321920d0..bb4a98c2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ dist-ssr public/assets public/assets_* !public/assets_default + +.vscode/vscode-chrome-debug-userdatadir diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..e80b447e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://local.revolt.chat:3000", + "webRoot": "${workspaceFolder}", + "runtimeExecutable": "/usr/bin/chromium", + "userDataDir": "${workspaceFolder}/.vscode/vscode-chrome-debug-userdatadir" + } + ] +} diff --git a/package.json b/package.json index 279af451..57e2a352 100644 --- a/package.json +++ b/package.json @@ -136,14 +136,12 @@ "react-helmet": "^6.1.0", "react-hook-form": "6.3.0", "react-overlapping-panels": "1.2.2", - "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scroll": "^1.8.2", "react-virtualized-auto-sizer": "^1.0.5", "react-virtuoso": "^1.10.4", - "redux": "^4.1.0", "revolt-api": "0.5.3-alpha.10", - "revolt.js": "5.2.0-patch.0", + "revolt.js": "5.2.1-patch.1", "rimraf": "^3.0.2", "sass": "^1.35.1", "shade-blend-color": "^1.0.0", diff --git a/src/components/common/AgeGate.tsx b/src/components/common/AgeGate.tsx index 03071212..11c2b56c 100644 --- a/src/components/common/AgeGate.tsx +++ b/src/components/common/AgeGate.tsx @@ -6,7 +6,8 @@ import styled from "styled-components"; import { Text } from "preact-i18n"; import { useState } from "preact/hooks"; -import { dispatch, getState } from "../../redux"; +import { useApplicationState } from "../../mobx/State"; +import { SECTION_NSFW } from "../../mobx/stores/Layout"; import Button from "../ui/Button"; import Checkbox from "../ui/Checkbox"; @@ -49,9 +50,7 @@ type Props = { export default observer((props: Props) => { const history = useHistory(); - const [consent, setConsent] = useState( - getState().sectionToggle["nsfw"] ?? false, - ); + const layout = useApplicationState().layout; const [ageGate, setAgeGate] = useState(false); if (ageGate || !props.gated) { @@ -81,26 +80,19 @@ export default observer((props: Props) => { { - setConsent(v); - if (v) { - dispatch({ - type: "SECTION_TOGGLE_SET", - id: "nsfw", - state: true, - }); - } else { - dispatch({ type: "SECTION_TOGGLE_UNSET", id: "nsfw" }); - } - }}> + checked={layout.getSectionState(SECTION_NSFW, false)} + onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}>
-
diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index 5eae1ed9..1765fb5f 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -7,7 +7,7 @@ import { useState } from "preact/hooks"; import { internalEmit } from "../../../lib/eventEmitter"; -import { QueuedMessage } from "../../../redux/reducers/queue"; +import { QueuedMessage } from "../../../mobx/stores/MessageQueue"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useClient } from "../../../context/revoltjs/RevoltClient"; diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 513e6280..330db243 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -21,7 +21,7 @@ import { } from "../../../lib/renderer/Singleton"; import { useApplicationState } from "../../../mobx/State"; -import { Reply } from "../../../redux/reducers/queue"; +import { Reply } from "../../../mobx/stores/MessageQueue"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { @@ -111,7 +111,7 @@ const Action = styled.div` const RE_SED = new RegExp("^s/([^])*/([^])*$"); // ! FIXME: add to app config and load from app config -export const CAN_UPLOAD_AT_ONCE = 4; +export const CAN_UPLOAD_AT_ONCE = 5; export default observer(({ channel }: Props) => { const state = useApplicationState(); diff --git a/src/components/common/messaging/bars/ReplyBar.tsx b/src/components/common/messaging/bars/ReplyBar.tsx index a2d10452..0e853e25 100644 --- a/src/components/common/messaging/bars/ReplyBar.tsx +++ b/src/components/common/messaging/bars/ReplyBar.tsx @@ -10,8 +10,9 @@ import { StateUpdater, useEffect } from "preact/hooks"; import { internalSubscribe } from "../../../../lib/eventEmitter"; -import { dispatch, getState } from "../../../../redux"; -import { Reply } from "../../../../redux/reducers/queue"; +import { useApplicationState } from "../../../../mobx/State"; +import { SECTION_MENTION } from "../../../../mobx/stores/Layout"; +import { Reply } from "../../../../mobx/stores/MessageQueue"; import IconButton from "../../../ui/IconButton"; @@ -81,6 +82,7 @@ const Base = styled.div` const MAX_REPLIES = 5; export default observer(({ channel, replies, setReplies }: Props) => { const client = channel.client; + const layout = useApplicationState().layout; // Event listener for adding new messages to reply bar. useEffect(() => { @@ -99,7 +101,7 @@ export default observer(({ channel, replies, setReplies }: Props) => { mention: message.author_id === client.user!._id ? false - : getState().sectionToggle.mention ?? false, + : layout.getSectionState("SECTION_MENTION", false), }, ]); }); @@ -181,11 +183,11 @@ export default observer(({ channel, replies, setReplies }: Props) => { }), ); - dispatch({ - type: "SECTION_TOGGLE_SET", - id: "mention", + layout.setSectionState( + SECTION_MENTION, state, - }); + false, + ); }}> diff --git a/src/components/common/messaging/embed/EmbedInvite.tsx b/src/components/common/messaging/embed/EmbedInvite.tsx index 830c5695..165a6f7b 100644 --- a/src/components/common/messaging/embed/EmbedInvite.tsx +++ b/src/components/common/messaging/embed/EmbedInvite.tsx @@ -10,8 +10,6 @@ import { useContext, useEffect, useState } from "preact/hooks"; import { defer } from "../../../../lib/defer"; import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; -import { dispatch } from "../../../../redux"; - import { AppContext, ClientStatus, @@ -33,7 +31,7 @@ const EmbedInviteBase = styled.div` align-items: center; padding: 0 12px; margin-top: 2px; - ${() => + ${() => isTouchscreenDevice && css` flex-wrap: wrap; @@ -44,19 +42,17 @@ const EmbedInviteBase = styled.div` > button { width: 100%; } - ` - } + `} `; const EmbedInviteDetails = styled.div` flex-grow: 1; padding-left: 12px; - ${() => + ${() => isTouchscreenDevice && css` width: calc(100% - 55px); - ` - } + `} `; const EmbedInviteName = styled.div` @@ -74,11 +70,10 @@ type Props = { code: string; }; -export function EmbedInvite(props: Props) { +export function EmbedInvite({ code }: Props) { const history = useHistory(); const client = useContext(AppContext); const status = useContext(StatusContext); - const code = props.code; const [processing, setProcessing] = useState(false); const [error, setError] = useState(undefined); const [joinError, setJoinError] = useState(undefined); @@ -124,7 +119,8 @@ export function EmbedInvite(props: Props) { {invite.server_name} - {invite.member_count.toLocaleString()} {invite.member_count === 1 ? "member" : "members"} + {invite.member_count.toLocaleString()}{" "} + {invite.member_count === 1 ? "member" : "members"} {processing ? ( @@ -151,10 +147,9 @@ export function EmbedInvite(props: Props) { defer(() => { if (server) { - dispatch({ - type: "UNREADS_MARK_MULTIPLE_READ", - channels: server.channel_ids, - }); + client.unreads!.markMultipleRead( + server.channel_ids, + ); history.push( `/server/${server._id}/channel/${invite.channel_id}`, @@ -172,7 +167,9 @@ export function EmbedInvite(props: Props) { setProcessing(false); } }}> - {client.servers.get(invite.server_id) ? "Joined" : "Join"} + {client.servers.get(invite.server_id) + ? "Joined" + : "Join"} )} diff --git a/src/components/navigation/LeftSidebar.tsx b/src/components/navigation/LeftSidebar.tsx index d9787a2e..309cb360 100644 --- a/src/components/navigation/LeftSidebar.tsx +++ b/src/components/navigation/LeftSidebar.tsx @@ -1,14 +1,16 @@ import { Route, Switch } from "react-router"; +import { useApplicationState } from "../../mobx/State"; +import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout"; + import SidebarBase from "./SidebarBase"; import HomeSidebar from "./left/HomeSidebar"; import ServerListSidebar from "./left/ServerListSidebar"; import ServerSidebar from "./left/ServerSidebar"; -import { useSelector } from "react-redux"; -import { State } from "../../redux"; export default function LeftSidebar() { - const isOpen = useSelector((state: State) => state.sectionToggle['sidebar_channels'] ?? true) + const layout = useApplicationState().layout; + const isOpen = layout.getSectionState(SIDEBAR_CHANNELS, true); return ( diff --git a/src/components/navigation/left/ServerSidebar.tsx b/src/components/navigation/left/ServerSidebar.tsx index 7ed142ef..b08e5597 100644 --- a/src/components/navigation/left/ServerSidebar.tsx +++ b/src/components/navigation/left/ServerSidebar.tsx @@ -11,7 +11,6 @@ import { internalEmit } from "../../../lib/eventEmitter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { useApplicationState } from "../../../mobx/State"; -import { connectState } from "../../../redux/connector"; import { useClient } from "../../../context/revoltjs/RevoltClient"; diff --git a/src/components/navigation/left/common.ts b/src/components/navigation/left/common.ts deleted file mode 100644 index ee5e5b66..00000000 --- a/src/components/navigation/left/common.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { reaction } from "mobx"; -import { Channel } from "revolt.js/dist/maps/Channels"; - -import { useLayoutEffect } from "preact/hooks"; - -import { dispatch } from "../../../redux"; -import { Unreads } from "../../../redux/reducers/unreads"; - -type UnreadProps = { - channel: Channel; - unreads: Unreads; -}; - -export function useUnreads({ channel, unreads }: UnreadProps) { - // const firstLoad = useRef(true); - useLayoutEffect(() => { - function checkUnread(target: Channel) { - if (!target) return; - if (target._id !== channel._id) return; - if ( - target.channel_type === "SavedMessages" || - target.channel_type === "VoiceChannel" - ) - return; - - const unread = unreads[channel._id]?.last_id; - if (target.last_message_id) { - if ( - !unread || - (unread && target.last_message_id.localeCompare(unread) > 0) - ) { - dispatch({ - type: "UNREADS_MARK_READ", - channel: channel._id, - message: target.last_message_id, - }); - - channel.ack(target.last_message_id); - } - } - } - - checkUnread(channel); - return reaction( - () => channel.last_message, - () => checkUnread(channel), - ); - }, [channel, unreads]); -} - -export function mapChannelWithUnread(channel: Channel, unreads: Unreads) { - const last_message_id = channel.last_message_id; - - let unread: "mention" | "unread" | undefined; - let alertCount: undefined | number; - if (last_message_id && unreads) { - const u = unreads[channel._id]; - if (u) { - if (u.mentions && u.mentions.length > 0) { - alertCount = u.mentions.length; - unread = "mention"; - } else if ( - u.last_id && - (last_message_id as string).localeCompare(u.last_id) > 0 - ) { - unread = "unread"; - } - } else { - unread = "unread"; - } - } - - return { - channel, - timestamp: last_message_id ?? channel._id, - unread, - alertCount, - }; -} diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 905dbd85..e25d6b38 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -167,7 +167,7 @@ export default function Modal(props: Props) { isModalClosing = animateClose; const onClose = useCallback(() => { setAnimateClose(true); - setTimeout(() => props.onClose?.(), 2e2); + setTimeout(() => props.onClose!(), 2e2); }, [setAnimateClose, props]); useEffect(() => internalSubscribe("Modal", "close", onClose), [onClose]); diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx index 55370c44..2c69ac9f 100644 --- a/src/context/Theme.tsx +++ b/src/context/Theme.tsx @@ -5,7 +5,6 @@ import { createGlobalStyle } from "styled-components"; import { useEffect } from "preact/hooks"; import { useApplicationState } from "../mobx/State"; -import { getState } from "../redux"; export type Variables = | "accent" @@ -280,28 +279,6 @@ export const PRESETS: Record = { }, }; -// todo: store used themes locally -export function getBaseTheme(name: string): Theme { - if (name in PRESETS) { - return PRESETS[name]; - } - - // TODO: properly initialize `themes` in state instead of letting it be undefined - const themes = getState().themes ?? {}; - - if (name in themes) { - const { theme } = themes[name]; - - return { - ...PRESETS[theme.light ? "light" : "dark"], - ...theme, - }; - } - - // how did we get here - return PRESETS["dark"]; -} - const keys = Object.keys(PRESETS.dark); const GlobalTheme = createGlobalStyle<{ theme: Theme }>` :root { diff --git a/src/context/index.tsx b/src/context/index.tsx index 1b6ee4ec..3f768da0 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -1,7 +1,5 @@ import { BrowserRouter as Router } from "react-router-dom"; -import State from "../redux/State"; - import { Children } from "../types/Preact"; import Locale from "./Locale"; import Theme from "./Theme"; @@ -15,14 +13,12 @@ import Client from "./revoltjs/RevoltClient"; export default function Context({ children }: { children: Children }) { return ( - - - - {children} - - - - + + + {children} + + + ); } diff --git a/src/context/intermediate/modals/ExternalLinkPrompt.tsx b/src/context/intermediate/modals/ExternalLinkPrompt.tsx index 42f73f28..09efd941 100644 --- a/src/context/intermediate/modals/ExternalLinkPrompt.tsx +++ b/src/context/intermediate/modals/ExternalLinkPrompt.tsx @@ -1,7 +1,6 @@ import { Text } from "preact-i18n"; import { useApplicationState } from "../../../mobx/State"; -import { dispatch } from "../../../redux"; import Modal from "../../../components/ui/Modal"; diff --git a/src/context/revoltjs/StateMonitor.tsx b/src/context/revoltjs/StateMonitor.tsx index 2f05b74e..061a7168 100644 --- a/src/context/revoltjs/StateMonitor.tsx +++ b/src/context/revoltjs/StateMonitor.tsx @@ -6,31 +6,28 @@ import { Message } from "revolt.js/dist/maps/Messages"; import { useContext, useEffect } from "preact/hooks"; import { useApplicationState } from "../../mobx/State"; -import { connectState } from "../../redux/connector"; -import { QueuedMessage } from "../../redux/reducers/queue"; import { setGlobalEmojiPack } from "../../components/common/Emoji"; import { AppContext } from "./RevoltClient"; -type Props = { - messages: QueuedMessage[]; -}; - -function StateMonitor(props: Props) { +export default function StateMonitor() { const client = useContext(AppContext); const state = useApplicationState(); useEffect(() => { function add(msg: Message) { if (!msg.nonce) return; - if (!props.messages.find((x) => x.id === 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, props.messages]); + }, [client]); // Set global emoji pack. useEffect(() => { @@ -40,9 +37,3 @@ function StateMonitor(props: Props) { return null; } - -export default connectState(StateMonitor, (state) => { - return { - messages: [...state.queue], - }; -}); diff --git a/src/context/revoltjs/SyncManager.tsx b/src/context/revoltjs/SyncManager.tsx index ec272ea5..4dd5b43f 100644 --- a/src/context/revoltjs/SyncManager.tsx +++ b/src/context/revoltjs/SyncManager.tsx @@ -1,25 +1,6 @@ /** * This file monitors changes to settings and syncs them to the server. */ -import isEqual from "lodash.isequal"; -import { UserSettings } from "revolt-api/types/Sync"; -import { ClientboundNotification } from "revolt.js/dist/websocket/notifications"; - -import { useCallback, useContext, useEffect, useMemo } from "preact/hooks"; - -import { dispatch } from "../../redux"; -import { connectState } from "../../redux/connector"; -import { Notifications } from "../../redux/reducers/notifications"; -import { Settings } from "../../redux/reducers/settings"; -import { - DEFAULT_ENABLED_SYNC, - SyncData, - SyncKeys, - SyncOptions, -} from "../../redux/reducers/sync"; - -import { Language } from "../Locale"; -import { AppContext, ClientStatus, StatusContext } from "./RevoltClient"; /*type Props = { settings: Settings; diff --git a/src/context/revoltjs/events.ts b/src/context/revoltjs/events.ts index 2556e76f..c39ed9fb 100644 --- a/src/context/revoltjs/events.ts +++ b/src/context/revoltjs/events.ts @@ -5,7 +5,6 @@ import { ClientboundNotification } from "revolt.js/dist/websocket/notifications" import { StateUpdater } from "preact/hooks"; import Auth from "../../mobx/stores/Auth"; -import { dispatch } from "../../redux"; import { ClientStatus } from "./RevoltClient"; @@ -46,29 +45,6 @@ export function registerEvents( attemptReconnect(); }, - packet: (packet: ClientboundNotification) => { - switch (packet.type) { - case "ChannelAck": { - dispatch({ - type: "UNREADS_MARK_READ", - channel: packet.id, - message: packet.message_id, - }); - break; - } - } - }, - - message: (message: Message) => { - if (message.mention_ids?.includes(client.user!._id)) { - dispatch({ - type: "UNREADS_MENTION", - channel: message.channel_id, - message: message._id, - }); - } - }, - ready: () => setStatus(ClientStatus.ONLINE), logout: () => { diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index 0b48eab7..cb657c20 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -33,14 +33,8 @@ import { Text } from "preact-i18n"; import { useContext } from "preact/hooks"; import { useApplicationState } from "../mobx/State"; -import { dispatch } from "../redux"; -import { connectState } from "../redux/connector"; -import { - getNotificationState, - Notifications, - NotificationState, -} from "../redux/reducers/notifications"; -import { QueuedMessage } from "../redux/reducers/queue"; +import { QueuedMessage } from "../mobx/stores/MessageQueue"; +import { NotificationState } from "../mobx/stores/NotificationOptions"; import { Screen, useIntermediate } from "../context/intermediate/Intermediate"; import { @@ -174,21 +168,19 @@ export default function ContextMenus() { ) return; - dispatch({ - type: "UNREADS_MARK_READ", - channel: data.channel._id, - message: data.channel.last_message_id!, - }); - - data.channel.ack(undefined, true); + client.unreads!.markRead( + data.channel._id, + data.channel.last_message_id!, + true, + true, + ); } break; case "mark_server_as_read": { - dispatch({ - type: "UNREADS_MARK_MULTIPLE_READ", - channels: data.server.channel_ids, - }); + client.unreads!.markMultipleRead( + data.server.channel_ids, + ); data.server.ack(); } @@ -439,16 +431,6 @@ export default function ContextMenus() { case "open_server_settings": history.push(`/server/${data.id}/settings`); break; - - case "set_notification_state": { - const { key, state } = data; - if (state) { - dispatch({ type: "NOTIFICATIONS_SET", key, state }); - } else { - dispatch({ type: "NOTIFICATIONS_REMOVE", key }); - } - break; - } } })().catch((err) => { openScreen({ id: "error", error: takeError(err) }); diff --git a/src/mobx/State.ts b/src/mobx/State.ts index 9c94cee3..bd69020f 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -14,6 +14,7 @@ import MessageQueue from "./stores/MessageQueue"; import NotificationOptions from "./stores/NotificationOptions"; import ServerConfig from "./stores/ServerConfig"; import Settings from "./stores/Settings"; +import Sync from "./stores/Sync"; /** * Handles global application state. @@ -28,6 +29,7 @@ export default class State { notifications: NotificationOptions; queue: MessageQueue; settings: Settings; + sync: Sync; private persistent: [string, Persistent][] = []; @@ -44,6 +46,7 @@ export default class State { this.notifications = new NotificationOptions(); this.queue = new MessageQueue(); this.settings = new Settings(); + this.sync = new Sync(); makeAutoObservable(this); this.registerListeners = this.registerListeners.bind(this); @@ -116,14 +119,25 @@ export default class State { } } -const StateContext = createContext(null!); - -export const StateContextProvider = StateContext.Provider; +var state: State; /** * Get the application state * @returns Application state */ export function useApplicationState() { - return useContext(StateContext); + if (!state) state = new State(); + return state; } + +/** + * + * Redux hydration: + * localForage.getItem("state").then((s) => { + if (s !== null) { + dispatch({ type: "__INIT", state: s as State }); + } + + state.hydrate().then(() => setLoaded(true)); + }); + */ diff --git a/src/mobx/implementation notes b/src/mobx/implementation notes deleted file mode 100644 index 36cefd3b..00000000 --- a/src/mobx/implementation notes +++ /dev/null @@ -1,25 +0,0 @@ -split settings per account(?) -multiple accounts need to be supported - -redux -> mobx migration (wipe existing redux data post-migration) - -> look into talking with other tabs to detect multiple instances -> (also use this to tell the user to close all tabs before updating) - -write new settings data structures for server-side ----- (deprecate existing API and replace with new endpoints?) -alternatively: keep using current system and eventually migrate -or: handle both incoming types of data and keep newer version (v1_prefix) -need to document these data structures - -provide state globally? perform all authentication from inside mobx -mobx parent holds client information and prepares us for first render - -reasoning for global: - -- we can't and won't have more than one of the application running in a single tab -- interactions become simpler -- all accounts will be managed from one place anyways - -things such as unreads can pass through this data store providing a host of -information, such as whether there are any alerts on channels, etc diff --git a/src/mobx/legacy/redux.ts b/src/mobx/legacy/redux.ts index 73154743..e9edfa5f 100644 --- a/src/mobx/legacy/redux.ts +++ b/src/mobx/legacy/redux.ts @@ -1,4 +1,4 @@ -import { AuthState } from "../../redux/reducers/auth"; +import { Session } from "revolt-api/types/Auth"; import { Language } from "../../context/Locale"; import { Fonts, MonospaceFonts, Overrides } from "../../context/Theme"; @@ -7,6 +7,7 @@ import { Data as DataAuth } from "../stores/Auth"; import { Data as DataLocaleOptions } from "../stores/LocaleOptions"; import { Data as DataNotificationOptions } from "../stores/NotificationOptions"; import { ISettings } from "../stores/Settings"; +import { Data as DataSync } from "../stores/Sync"; export type LegacyTheme = Overrides & { light?: boolean; @@ -39,7 +40,29 @@ export interface LegacySyncData { notifications?: LegacyNotifications; } -function legacyMigrateAuth(auth: AuthState): DataAuth { +export type LegacySyncKeys = + | "theme" + | "appearance" + | "locale" + | "notifications"; + +export interface LegacySyncOptions { + disabled?: LegacySyncKeys[]; + revision?: { + [key: string]: number; + }; +} + +export interface LegacyAuthState { + accounts: { + [key: string]: { + session: Session; + }; + }; + active?: string; +} + +function legacyMigrateAuth(auth: LegacyAuthState): DataAuth { return { current: auth.active, sessions: auth.accounts, @@ -82,3 +105,12 @@ function legacyMigrateNotification( channel, }; } + +function legacyMigrateSync(sync: LegacySyncOptions): DataSync { + return { + disabled: sync.disabled ?? [], + revision: { + ...sync.revision, + }, + }; +} diff --git a/src/mobx/stores/Layout.ts b/src/mobx/stores/Layout.ts index cd17a4ac..80d03109 100644 --- a/src/mobx/stores/Layout.ts +++ b/src/mobx/stores/Layout.ts @@ -12,6 +12,11 @@ export interface Data { openSections?: Record; } +export const SIDEBAR_MEMBERS = "sidebar_members"; +export const SIDEBAR_CHANNELS = "sidebar_channels"; +export const SECTION_MENTION = "mention"; +export const SECTION_NSFW = "nsfw"; + /** * Keeps track of the last open channels, tabs, etc. * Handles providing good UX experience on navigating @@ -165,4 +170,13 @@ export default class Layout implements Store, Persistent { this.openSections.set(id, value); } } + + /** + * Toggle state of a section. + * @param id Section ID + * @param def Default state value + */ + @action toggleSectionState(id: string, def?: boolean) { + this.setSectionState(id, !this.getSectionState(id, def)); + } } diff --git a/src/mobx/stores/Settings.ts b/src/mobx/stores/Settings.ts index 80a75253..b64e4b9f 100644 --- a/src/mobx/stores/Settings.ts +++ b/src/mobx/stores/Settings.ts @@ -78,10 +78,14 @@ export default class Settings implements Store, Persistent { /** * Get a settings key. * @param key Colon-divided key + * @param defaultValue Default value if not present * @returns Value at key */ - @computed get(key: T) { - return this.data.get(key) as ISettings[T] | undefined; + @computed get( + key: T, + defaultValue?: ISettings[T], + ) { + return (this.data.get(key) as ISettings[T] | undefined) ?? defaultValue; } @action remove(key: T) { diff --git a/src/mobx/stores/Sync.ts b/src/mobx/stores/Sync.ts index 42d76edc..b0b47065 100644 --- a/src/mobx/stores/Sync.ts +++ b/src/mobx/stores/Sync.ts @@ -23,6 +23,9 @@ export const SYNC_KEYS: SyncKeys[] = [ export interface Data { disabled: SyncKeys[]; + revision: { + [key: string]: number; + }; } /** @@ -30,12 +33,14 @@ export interface Data { */ export default class Sync implements Store, Persistent { private disabled: ObservableSet; + private revision: ObservableMap; /** * Construct new Sync store. */ constructor() { this.disabled = new ObservableSet(); + this.revision = new ObservableMap(); makeAutoObservable(this); this.isEnabled = this.isEnabled.bind(this); } @@ -47,6 +52,7 @@ export default class Sync implements Store, Persistent { toJSON() { return { enabled: [...this.disabled], + revision: mapToRecord(this.revision), }; } @@ -58,6 +64,22 @@ export default class Sync implements Store, Persistent { } } + @action enable(key: SyncKeys) { + this.disabled.delete(key); + } + + @action disable(key: SyncKeys) { + this.disabled.add(key); + } + + @action toggle(key: SyncKeys) { + if (this.isEnabled(key)) { + this.disable(key); + } else { + this.enable(key); + } + } + @computed isEnabled(key: SyncKeys) { return !this.disabled.has(key); } diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx index cf162c6d..8695f5c3 100644 --- a/src/pages/channels/Channel.tsx +++ b/src/pages/channels/Channel.tsx @@ -7,11 +7,12 @@ import { Channel as ChannelI } from "revolt.js/dist/maps/Channels"; import styled from "styled-components"; import { Text } from "preact-i18n"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect } from "preact/hooks"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; -import { dispatch, getState } from "../../redux"; +import { useApplicationState } from "../../mobx/State"; +import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout"; import { useClient } from "../../context/revoltjs/RevoltClient"; @@ -83,15 +84,8 @@ export function Channel({ id }: { id: string }) { return ; } -const MEMBERS_SIDEBAR_KEY = "sidebar_members"; -const CHANNELS_SIDEBAR_KEY = "sidebar_channels"; const TextChannel = observer(({ channel }: { channel: ChannelI }) => { - const [showMembers, setMembers] = useState( - getState().sectionToggle[MEMBERS_SIDEBAR_KEY] ?? true, - ); - const [showChannels, setChannels] = useState( - getState().sectionToggle[CHANNELS_SIDEBAR_KEY] ?? true, - ); + const layout = useApplicationState().layout; // Mark channel as read. useEffect(() => { @@ -121,45 +115,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => { channel.nsfw ) }> - { - setMembers(!showMembers); - - if (showMembers) { - dispatch({ - type: "SECTION_TOGGLE_SET", - id: MEMBERS_SIDEBAR_KEY, - state: false, - }); - } else { - dispatch({ - type: "SECTION_TOGGLE_UNSET", - id: MEMBERS_SIDEBAR_KEY, - }); - } - }} - toggleChannelSidebar={() => { - if (isTouchscreenDevice) { - return; - } - - setChannels(!showChannels); - - if (showChannels) { - dispatch({ - type: "SECTION_TOGGLE_SET", - id: CHANNELS_SIDEBAR_KEY, - state: false, - }); - } else { - dispatch({ - type: "SECTION_TOGGLE_UNSET", - id: CHANNELS_SIDEBAR_KEY, - }); - } - }} - /> + @@ -168,7 +124,10 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => { - {!isTouchscreenDevice && showMembers && } + {!isTouchscreenDevice && + layout.getSectionState(SIDEBAR_MEMBERS, true) && ( + + )} ); diff --git a/src/pages/channels/ChannelHeader.tsx b/src/pages/channels/ChannelHeader.tsx index 84f188af..3fa8e835 100644 --- a/src/pages/channels/ChannelHeader.tsx +++ b/src/pages/channels/ChannelHeader.tsx @@ -1,4 +1,4 @@ -import { At, Hash, Menu } from "@styled-icons/boxicons-regular"; +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/dist/maps/Channels"; @@ -7,6 +7,9 @@ import styled, { css } from "styled-components"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; +import { useApplicationState } from "../../mobx/State"; +import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout"; + import { useIntermediate } from "../../context/intermediate/Intermediate"; import { getChannelName } from "../../context/revoltjs/util"; @@ -69,14 +72,16 @@ const IconConainer = styled.div` cursor: pointer; color: var(--secondary-foreground); - ${!isTouchscreenDevice && css` + ${!isTouchscreenDevice && + css` &:hover { color: var(--foreground); } `} -` +`; -export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: ChannelHeaderProps) => { +export default observer(({ channel }: ChannelHeaderProps) => { + const layout = useApplicationState().layout; const { openScreen } = useIntermediate(); const name = getChannelName(channel); @@ -100,7 +105,12 @@ export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: Chann return (
- {icon} + + layout.toggleSectionState(SIDEBAR_MEMBERS, true) + }> + {icon} + {name} {isTouchscreenDevice && @@ -143,7 +153,7 @@ export default observer(({ channel, toggleSidebar, toggleChannelSidebar }: Chann )} - +
); }); diff --git a/src/pages/channels/actions/HeaderActions.tsx b/src/pages/channels/actions/HeaderActions.tsx index 514a1b2e..0466e0ca 100644 --- a/src/pages/channels/actions/HeaderActions.tsx +++ b/src/pages/channels/actions/HeaderActions.tsx @@ -14,6 +14,9 @@ import { internalEmit } from "../../../lib/eventEmitter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { voiceState, VoiceStatus } from "../../../lib/vortex/VoiceState"; +import { useApplicationState } from "../../../mobx/State"; +import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout"; + import { useIntermediate } from "../../../context/intermediate/Intermediate"; import UpdateIndicator from "../../../components/common/UpdateIndicator"; @@ -21,10 +24,8 @@ import IconButton from "../../../components/ui/IconButton"; import { ChannelHeaderProps } from "../ChannelHeader"; -export default function HeaderActions({ - channel, - toggleSidebar, -}: ChannelHeaderProps) { +export default function HeaderActions({ channel }: ChannelHeaderProps) { + const layout = useApplicationState().layout; const { openScreen } = useIntermediate(); const history = useHistory(); @@ -40,7 +41,7 @@ export default function HeaderActions({ if (isTouchscreenDevice) { openRightSidebar(); } else { - toggleSidebar?.(); + layout.toggleSectionState(SIDEBAR_MEMBERS, true); } } diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index 47f0f1ae..2a7f629c 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -10,15 +10,12 @@ import styled from "styled-components"; import { decodeTime } from "ulid"; import { Text } from "preact-i18n"; -import { memo } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter"; import { ChannelRenderer } from "../../../lib/renderer/Singleton"; import { useApplicationState } from "../../../mobx/State"; -import { connectState } from "../../../redux/connector"; -import { QueuedMessage } from "../../../redux/reducers/queue"; import RequiresOnline from "../../../context/revoltjs/RequiresOnline"; import { useClient } from "../../../context/revoltjs/RevoltClient"; diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index 812ed082..f188295d 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -4,11 +4,12 @@ import styled, { css } from "styled-components"; import styles from "./Home.module.scss"; import { Text } from "preact-i18n"; -import { useContext, useState } from "preact/hooks"; +import { useContext } from "preact/hooks"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; -import { dispatch, getState } from "../../redux"; +import { useApplicationState } from "../../mobx/State"; +import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout"; import { AppContext } from "../../context/revoltjs/RevoltClient"; @@ -18,8 +19,6 @@ import Tooltip from "../../components/common/Tooltip"; import Header from "../../components/ui/Header"; import CategoryButton from "../../components/ui/fluent/CategoryButton"; -const CHANNELS_SIDEBAR_KEY = "sidebar_channels"; - const IconConainer = styled.div` cursor: pointer; color: var(--secondary-foreground); @@ -34,29 +33,14 @@ const IconConainer = styled.div` export default function Home() { const client = useContext(AppContext); - const [showChannels, setChannels] = useState( - getState().sectionToggle[CHANNELS_SIDEBAR_KEY] ?? true, - ); + const layout = useApplicationState().layout; const toggleChannelSidebar = () => { if (isTouchscreenDevice) { return; } - setChannels(!showChannels); - - if (showChannels) { - dispatch({ - type: "SECTION_TOGGLE_SET", - id: CHANNELS_SIDEBAR_KEY, - state: false, - }); - } else { - dispatch({ - type: "SECTION_TOGGLE_UNSET", - id: CHANNELS_SIDEBAR_KEY, - }); - } + layout.toggleSectionState(SIDEBAR_CHANNELS, true); }; return ( diff --git a/src/pages/invite/Invite.tsx b/src/pages/invite/Invite.tsx index be29427b..daba4967 100644 --- a/src/pages/invite/Invite.tsx +++ b/src/pages/invite/Invite.tsx @@ -10,8 +10,6 @@ import { useContext, useEffect, useState } from "preact/hooks"; import { defer } from "../../lib/defer"; import { TextReact } from "../../lib/i18n"; -import { dispatch } from "../../redux"; - import RequiresOnline from "../../context/revoltjs/RequiresOnline"; import { AppContext, @@ -168,11 +166,9 @@ export default function Invite() { defer(() => { if (server) { - dispatch({ - type: "UNREADS_MARK_MULTIPLE_READ", - channels: - server.channel_ids, - }); + client.unreads!.markMultipleRead( + server.channel_ids, + ); history.push( `/server/${server._id}/channel/${invite.channel_id}`, diff --git a/src/pages/settings/panes/Audio.tsx b/src/pages/settings/panes/Audio.tsx index 35776b89..ecc7f4c1 100644 --- a/src/pages/settings/panes/Audio.tsx +++ b/src/pages/settings/panes/Audio.tsx @@ -6,8 +6,6 @@ import { TextReact } from "../../../lib/i18n"; import { stopPropagation } from "../../../lib/stopPropagation"; import { voiceState } from "../../../lib/vortex/VoiceState"; -import { connectState } from "../../../redux/connector"; - import Button from "../../../components/ui/Button"; import ComboBox from "../../../components/ui/ComboBox"; import Overline from "../../../components/ui/Overline"; @@ -17,7 +15,7 @@ const constraints = { audio: true }; // TODO: do not rewrite this code until voice is rewritten! -export function Component() { +export function Audio() { const [mediaStream, setMediaStream] = useState( undefined, ); @@ -163,7 +161,3 @@ function changeAudioDevice(deviceId: string, deviceType: string) { window.localStorage.setItem("audioOutputDevice", deviceId); } } - -export const Audio = connectState(Component, () => { - return; -}); diff --git a/src/pages/settings/panes/Languages.tsx b/src/pages/settings/panes/Languages.tsx index 98f76e0d..6947bb12 100644 --- a/src/pages/settings/panes/Languages.tsx +++ b/src/pages/settings/panes/Languages.tsx @@ -4,12 +4,7 @@ import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; import { useMemo } from "preact/hooks"; -import PaintCounter from "../../../lib/PaintCounter"; - import { useApplicationState } from "../../../mobx/State"; -import LocaleOptions from "../../../mobx/stores/LocaleOptions"; -import { dispatch } from "../../../redux"; -import { connectState } from "../../../redux/connector"; import { Language, diff --git a/src/pages/settings/panes/Notifications.tsx b/src/pages/settings/panes/Notifications.tsx index 879f7c62..6643dd37 100644 --- a/src/pages/settings/panes/Notifications.tsx +++ b/src/pages/settings/panes/Notifications.tsx @@ -5,23 +5,16 @@ import { useContext, useEffect, useState } from "preact/hooks"; import { urlBase64ToUint8Array } from "../../../lib/conversion"; import { useApplicationState } from "../../../mobx/State"; -import { dispatch } from "../../../redux"; -import { connectState } from "../../../redux/connector"; -import { NotificationOptions } from "../../../redux/reducers/settings"; import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { AppContext } from "../../../context/revoltjs/RevoltClient"; import Checkbox from "../../../components/ui/Checkbox"; -interface Props { - options?: NotificationOptions; -} - -export function Component({ options }: Props) { +export function Notifications() { const client = useContext(AppContext); const { openScreen } = useIntermediate(); - const sounds = useApplicationState().settings.sounds; + const settings = useApplicationState().settings; const [pushEnabled, setPushEnabled] = useState( undefined, ); @@ -43,7 +36,7 @@ export function Component({ options }: Props) { } @@ -51,6 +44,7 @@ export function Component({ options }: Props) { if (desktopEnabled) { const permission = await Notification.requestPermission(); + if (permission !== "granted") { return openScreen({ id: "error", @@ -59,10 +53,7 @@ export function Component({ options }: Props) { } } - dispatch({ - type: "SETTINGS_SET_NOTIFICATION_OPTIONS", - options: { desktopEnabled }, - }); + settings.set("notifications:desktop", desktopEnabled); }}> @@ -115,20 +106,16 @@ export function Component({ options }: Props) {

- {sounds.getState().map(({ id, enabled }) => ( + {settings.sounds.getState().map(({ id, enabled }) => ( sounds.setEnabled(id, enabled)}> + onChange={(enabled) => + settings.sounds.setEnabled(id, enabled) + }> ))} ); } - -export const Notifications = connectState(Component, (state) => { - return { - options: state.settings.notification, - }; -}); diff --git a/src/pages/settings/panes/Sync.tsx b/src/pages/settings/panes/Sync.tsx index c77b5df2..1eff8640 100644 --- a/src/pages/settings/panes/Sync.tsx +++ b/src/pages/settings/panes/Sync.tsx @@ -1,17 +1,16 @@ +import { observer } from "mobx-react-lite"; + import styles from "./Panes.module.scss"; import { Text } from "preact-i18n"; -import { dispatch } from "../../../redux"; -import { connectState } from "../../../redux/connector"; -import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync"; +import { useApplicationState } from "../../../mobx/State"; +import { SyncKeys } from "../../../mobx/stores/Sync"; import Checkbox from "../../../components/ui/Checkbox"; -interface Props { - options?: SyncOptions; -} +export const Sync = observer(() => { + const sync = useApplicationState().sync; -export function Component(props: Props) { return (

@@ -27,31 +26,16 @@ export function Component(props: Props) { ).map(([key, title]) => ( } - onChange={(enabled) => - dispatch({ - type: enabled - ? "SYNC_ENABLE_KEY" - : "SYNC_DISABLE_KEY", - key, - }) - }> + onChange={() => sync.toggle(key)}> ))}

); -} - -export const Sync = connectState(Component, (state) => { - return { - options: state.sync, - }; }); diff --git a/src/pages/settings/panes/ThemeShop.tsx b/src/pages/settings/panes/ThemeShop.tsx index 11965e64..10220ec7 100644 --- a/src/pages/settings/panes/ThemeShop.tsx +++ b/src/pages/settings/panes/ThemeShop.tsx @@ -3,9 +3,8 @@ import styled from "styled-components"; import { useEffect, useState } from "preact/hooks"; import { useApplicationState } from "../../../mobx/State"; -import { dispatch } from "../../../redux"; -import { Theme, generateVariables, ThemeOptions } from "../../../context/Theme"; +import { Theme, generateVariables } from "../../../context/Theme"; import Tip from "../../../components/ui/Tip"; import previewPath from "../assets/preview.svg"; diff --git a/src/redux/State.tsx b/src/redux/State.tsx deleted file mode 100644 index 7edfd4b7..00000000 --- a/src/redux/State.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import localForage from "localforage"; -import { Provider } from "react-redux"; - -import { useEffect, useRef, useState } from "preact/hooks"; - -import MobXState, { StateContextProvider } from "../mobx/State"; - -import { dispatch, State, store } from "."; -import { Children } from "../types/Preact"; - -interface Props { - children: Children; -} - -/** - * Component for loading application state. - * @param props Provided children - */ -export default function StateLoader(props: Props) { - const [loaded, setLoaded] = useState(false); - const { current: state } = useRef(new MobXState()); - - // Globally expose the application state. - useEffect(() => { - (window as unknown as Record).state = state; - }, [state]); - - useEffect(() => { - localForage.getItem("state").then((s) => { - if (s !== null) { - dispatch({ type: "__INIT", state: s as State }); - } - - state.hydrate().then(() => setLoaded(true)); - }); - }, []); - - if (!loaded) return null; - - useEffect(state.registerListeners); - - return ( - - - {props.children} - - - ); -} diff --git a/src/redux/connector.tsx b/src/redux/connector.tsx deleted file mode 100644 index 699238d6..00000000 --- a/src/redux/connector.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { connect, ConnectedComponent } from "react-redux"; - -import { h } from "preact"; -import { memo } from "preact/compat"; - -import { State } from "."; - -export function connectState( - component: (props: any) => h.JSX.Element | null, - mapKeys: (state: State, props: T) => any, - memoize?: boolean, -): ConnectedComponent<(props: any) => h.JSX.Element | null, T> { - const c = connect(mapKeys)(component); - return memoize ? memo(c) : c; -} diff --git a/src/redux/index.ts b/src/redux/index.ts deleted file mode 100644 index 2e5b7d84..00000000 --- a/src/redux/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import localForage from "localforage"; -import { createStore } from "redux"; -import { RevoltConfiguration } from "revolt-api/types/Core"; - -import { Language } from "../context/Locale"; - -import rootReducer, { Action } from "./reducers"; -import { AuthState } from "./reducers/auth"; -import { Drafts } from "./reducers/drafts"; -import { ExperimentOptions } from "./reducers/experiments"; -import { LastOpened } from "./reducers/last_opened"; -import { Notifications } from "./reducers/notifications"; -import { QueuedMessage } from "./reducers/queue"; -import { SectionToggle } from "./reducers/section_toggle"; -import { Settings } from "./reducers/settings"; -import { SyncOptions } from "./reducers/sync"; -import { Themes } from "./reducers/themes"; -import { TrustedLinks } from "./reducers/trusted_links"; -import { Unreads } from "./reducers/unreads"; - -export type State = { - config: RevoltConfiguration; - locale: Language; - auth: AuthState; - settings: Settings; - unreads: Unreads; - queue: QueuedMessage[]; - drafts: Drafts; - sync: SyncOptions; - experiments: ExperimentOptions; - lastOpened: LastOpened; - notifications: Notifications; - sectionToggle: SectionToggle; - trustedLinks: TrustedLinks; - themes: Themes; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const store = createStore((state: any, action: any) => { - if (import.meta.env.DEV) { - console.debug("State Update:", action); - } - - if (action.type === "__INIT") { - return action.state; - } - - return rootReducer(state, action); -}); - -// Save state using localForage. -store.subscribe(() => { - const { - config, - locale, - auth, - settings, - unreads, - queue, - drafts, - sync, - experiments, - lastOpened, - notifications, - sectionToggle, - trustedLinks, - themes, - } = store.getState() as State; - - localForage.setItem("state", { - config, - locale, - auth, - settings, - unreads, - queue, - drafts, - sync, - experiments, - lastOpened, - notifications, - sectionToggle, - trustedLinks, - themes, - }); -}); - -export function dispatch(action: Action) { - store.dispatch(action); -} - -export function getState(): State { - return store.getState(); -} diff --git a/src/redux/reducers/auth.ts b/src/redux/reducers/auth.ts deleted file mode 100644 index 2bb2ce99..00000000 --- a/src/redux/reducers/auth.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Session } from "revolt-api/types/Auth"; - -export interface AuthState { - accounts: { - [key: string]: { - session: Session; - }; - }; - active?: string; -} - -export type AuthAction = - | { type: undefined } - | { - type: "LOGIN"; - session: Session; - } - | { - type: "LOGOUT"; - user_id?: string; - }; - -export function auth( - state = { accounts: {} } as AuthState, - action: AuthAction, -): AuthState { - switch (action.type) { - case "LOGIN": - return { - accounts: { - ...state.accounts, - [action.session.user_id]: { - session: action.session, - }, - }, - active: action.session.user_id, - }; - case "LOGOUT": { - const accounts = Object.assign({}, state.accounts); - action.user_id && delete accounts[action.user_id]; - - return { - accounts, - }; - } - default: - return state; - } -} diff --git a/src/redux/reducers/drafts.ts b/src/redux/reducers/drafts.ts deleted file mode 100644 index 4f36a846..00000000 --- a/src/redux/reducers/drafts.ts +++ /dev/null @@ -1,35 +0,0 @@ -export type Drafts = { [key: string]: string }; - -export type DraftAction = - | { type: undefined } - | { - type: "SET_DRAFT"; - channel: string; - content: string; - } - | { - type: "CLEAR_DRAFT"; - channel: string; - } - | { - type: "RESET"; - }; - -export function drafts(state: Drafts = {}, action: DraftAction): Drafts { - switch (action.type) { - case "SET_DRAFT": - return { - ...state, - [action.channel]: action.content, - }; - case "CLEAR_DRAFT": { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [action.channel]: _, ...newState } = state; - return newState; - } - case "RESET": - return {}; - default: - return state; - } -} diff --git a/src/redux/reducers/experiments.ts b/src/redux/reducers/experiments.ts deleted file mode 100644 index 7d081f86..00000000 --- a/src/redux/reducers/experiments.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { getState } from ".."; - -export type Experiments = "search" | "theme_shop"; - -export const AVAILABLE_EXPERIMENTS: Experiments[] = ["theme_shop"]; - -export const EXPERIMENTS: { - [key in Experiments]: { title: string; description: string }; -} = { - search: { - title: "Search", - description: "Allows you to search for messages in channels.", - }, - theme_shop: { - title: "Theme Shop", - description: "Allows you to access and set user submitted themes.", - }, -}; - -export interface ExperimentOptions { - enabled?: Experiments[]; -} - -export type ExperimentsAction = - | { type: undefined } - | { - type: "EXPERIMENTS_ENABLE"; - key: Experiments; - } - | { - type: "EXPERIMENTS_DISABLE"; - key: Experiments; - }; - -export function experiments( - state = {} as ExperimentOptions, - action: ExperimentsAction, -): ExperimentOptions { - switch (action.type) { - case "EXPERIMENTS_ENABLE": - return { - ...state, - enabled: [ - ...(state.enabled ?? []) - .filter((x) => AVAILABLE_EXPERIMENTS.includes(x)) - .filter((v) => v !== action.key), - action.key, - ], - }; - case "EXPERIMENTS_DISABLE": - return { - ...state, - enabled: state.enabled - ?.filter((v) => v !== action.key) - .filter((x) => AVAILABLE_EXPERIMENTS.includes(x)), - }; - default: - return state; - } -} - -export function isExperimentEnabled( - name: Experiments, - experiments: ExperimentOptions = getState().experiments, -) { - return experiments.enabled?.includes(name) ?? false; -} diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts deleted file mode 100644 index aa8be8f4..00000000 --- a/src/redux/reducers/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { combineReducers } from "redux"; - -import { State } from ".."; -import { auth, AuthAction } from "./auth"; -import { drafts, DraftAction } from "./drafts"; -import { experiments, ExperimentsAction } from "./experiments"; -import { lastOpened, LastOpenedAction } from "./last_opened"; -import { locale, LocaleAction } from "./locale"; -import { notifications, NotificationsAction } from "./notifications"; -import { queue, QueueAction } from "./queue"; -import { sectionToggle, SectionToggleAction } from "./section_toggle"; -import { config, ConfigAction } from "./server_config"; -import { settings, SettingsAction } from "./settings"; -import { sync, SyncAction } from "./sync"; -import { themes, ThemesAction } from "./themes"; -import { trustedLinks, TrustedLinksAction } from "./trusted_links"; -import { unreads, UnreadsAction } from "./unreads"; - -export default combineReducers({ - config, - locale, - auth, - settings, - unreads, - queue, - drafts, - sync, - experiments, - lastOpened, - notifications, - sectionToggle, - trustedLinks, - themes, -}); - -export type Action = - | ConfigAction - | LocaleAction - | AuthAction - | SettingsAction - | UnreadsAction - | QueueAction - | DraftAction - | SyncAction - | ExperimentsAction - | LastOpenedAction - | NotificationsAction - | SectionToggleAction - | TrustedLinksAction - | ThemesAction - | { type: "__INIT"; state: State }; diff --git a/src/redux/reducers/last_opened.ts b/src/redux/reducers/last_opened.ts deleted file mode 100644 index e49c444e..00000000 --- a/src/redux/reducers/last_opened.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface LastOpened { - [key: string]: string; -} - -export type LastOpenedAction = - | { type: undefined } - | { - type: "LAST_OPENED_SET"; - parent: string; - child: string; - } - | { - type: "RESET"; - }; - -export function lastOpened( - state = {} as LastOpened, - action: LastOpenedAction, -): LastOpened { - switch (action.type) { - case "LAST_OPENED_SET": { - return { - ...state, - [action.parent]: action.child, - }; - } - case "RESET": - return {}; - default: - return state; - } -} diff --git a/src/redux/reducers/locale.ts b/src/redux/reducers/locale.ts deleted file mode 100644 index 5e8e4ec0..00000000 --- a/src/redux/reducers/locale.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Language, Languages } from "../../context/Locale"; - -import type { SyncUpdateAction } from "./sync"; - -export type LocaleAction = - | { type: undefined } - | { - type: "SET_LOCALE"; - locale: Language; - } - | SyncUpdateAction; - -export function findLanguage(lang?: string): Language { - if (!lang) { - if (typeof navigator === "undefined") { - lang = Language.ENGLISH; - } else { - lang = navigator.language; - } - } - - const code = lang.replace("-", "_"); - const short = code.split("_")[0]; - - const values = []; - for (const key in Language) { - const value = Language[key as keyof typeof Language]; - - // Skip alternative/joke languages - if (Languages[value].cat === "alt") continue; - - values.push(value); - if (value.startsWith(code)) { - return value as Language; - } - } - - for (const value of values.reverse()) { - if (value.startsWith(short)) { - return value as Language; - } - } - - return Language.ENGLISH; -} - -export function locale(state = findLanguage(), action: LocaleAction): Language { - switch (action.type) { - case "SET_LOCALE": - return action.locale; - case "SYNC_UPDATE": - return (action.update.locale?.[1] ?? state) as Language; - default: - return state; - } -} diff --git a/src/redux/reducers/notifications.ts b/src/redux/reducers/notifications.ts deleted file mode 100644 index 76e3679b..00000000 --- a/src/redux/reducers/notifications.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Channel } from "revolt.js/dist/maps/Channels"; -import { Message } from "revolt.js/dist/maps/Messages"; - -import type { SyncUpdateAction } from "./sync"; - -export type NotificationState = "all" | "mention" | "none" | "muted"; - -export type Notifications = { - [key: string]: NotificationState; -}; - -export const DEFAULT_STATES: { - [key in Channel["channel_type"]]: NotificationState; -} = { - SavedMessages: "all", - DirectMessage: "all", - Group: "all", - TextChannel: "mention", - VoiceChannel: "mention", -}; - -export function getNotificationState( - notifications: Notifications, - channel: Channel, -) { - return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type]; -} - -export function shouldNotify( - state: NotificationState, - message: Message, - user_id: string, -) { - switch (state) { - case "muted": - case "none": - return false; - case "mention": { - if (!message.mention_ids?.includes(user_id)) return false; - } - } - - return true; -} - -export type NotificationsAction = - | { type: undefined } - | { - type: "NOTIFICATIONS_SET"; - key: string; - state: NotificationState; - } - | { - type: "NOTIFICATIONS_REMOVE"; - key: string; - } - | SyncUpdateAction - | { - type: "RESET"; - }; - -export function notifications( - state = {} as Notifications, - action: NotificationsAction, -): Notifications { - switch (action.type) { - case "NOTIFICATIONS_SET": - return { - ...state, - [action.key]: action.state, - }; - case "NOTIFICATIONS_REMOVE": { - const { [action.key]: _, ...newState } = state; - return newState; - } - case "SYNC_UPDATE": - return action.update.notifications?.[1] ?? state; - case "RESET": - return {}; - default: - return state; - } -} diff --git a/src/redux/reducers/queue.ts b/src/redux/reducers/queue.ts deleted file mode 100644 index c4059e05..00000000 --- a/src/redux/reducers/queue.ts +++ /dev/null @@ -1,115 +0,0 @@ -export enum QueueStatus { - SENDING = "sending", - ERRORED = "errored", -} - -export interface Reply { - id: string; - mention: boolean; -} - -export type QueuedMessageData = { - _id: string; - author: string; - channel: string; - - content: string; - replies: Reply[]; -}; - -export interface QueuedMessage { - id: string; - channel: string; - data: QueuedMessageData; - status: QueueStatus; - error?: string; -} - -export type QueueAction = - | { type: undefined } - | { - type: "QUEUE_ADD"; - nonce: string; - channel: string; - message: QueuedMessageData; - } - | { - type: "QUEUE_FAIL"; - nonce: string; - error: string; - } - | { - type: "QUEUE_START"; - nonce: string; - } - | { - type: "QUEUE_REMOVE"; - nonce: string; - } - | { - type: "QUEUE_DROP_ALL"; - } - | { - type: "QUEUE_FAIL_ALL"; - } - | { - type: "RESET"; - }; - -export function queue( - state: QueuedMessage[] = [], - action: QueueAction, -): QueuedMessage[] { - switch (action.type) { - case "QUEUE_ADD": { - return [ - ...state.filter((x) => x.id !== action.nonce), - { - id: action.nonce, - data: action.message, - channel: action.channel, - status: QueueStatus.SENDING, - }, - ]; - } - case "QUEUE_FAIL": { - const entry = state.find( - (x) => x.id === action.nonce, - ) as QueuedMessage; - return [ - ...state.filter((x) => x.id !== action.nonce), - { - ...entry, - status: QueueStatus.ERRORED, - error: action.error, - }, - ]; - } - case "QUEUE_START": { - const entry = state.find( - (x) => x.id === action.nonce, - ) as QueuedMessage; - return [ - ...state.filter((x) => x.id !== action.nonce), - { - ...entry, - status: QueueStatus.SENDING, - }, - ]; - } - case "QUEUE_REMOVE": - return state.filter((x) => x.id !== action.nonce); - case "QUEUE_FAIL_ALL": - return state.map((x) => { - return { - ...x, - status: QueueStatus.ERRORED, - }; - }); - case "QUEUE_DROP_ALL": - case "RESET": - return []; - default: - return state; - } -} diff --git a/src/redux/reducers/section_toggle.ts b/src/redux/reducers/section_toggle.ts deleted file mode 100644 index cff440d0..00000000 --- a/src/redux/reducers/section_toggle.ts +++ /dev/null @@ -1,40 +0,0 @@ -export interface SectionToggle { - [key: string]: boolean; -} - -export type SectionToggleAction = - | { type: undefined } - | { - type: "SECTION_TOGGLE_SET"; - id: string; - state: boolean; - } - | { - type: "SECTION_TOGGLE_UNSET"; - id: string; - } - | { - type: "RESET"; - }; - -export function sectionToggle( - state = {} as SectionToggle, - action: SectionToggleAction, -): SectionToggle { - switch (action.type) { - case "SECTION_TOGGLE_SET": { - return { - ...state, - [action.id]: action.state, - }; - } - case "SECTION_TOGGLE_UNSET": { - const { [action.id]: _, ...newState } = state; - return newState; - } - case "RESET": - return {}; - default: - return state; - } -} diff --git a/src/redux/reducers/server_config.ts b/src/redux/reducers/server_config.ts deleted file mode 100644 index 916cb2d7..00000000 --- a/src/redux/reducers/server_config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RevoltConfiguration } from "revolt-api/types/Core"; - -export type ConfigAction = - | { type: undefined } - | { - type: "SET_CONFIG"; - config: RevoltConfiguration; - }; - -export function config( - state = {} as RevoltConfiguration, - action: ConfigAction, -): RevoltConfiguration { - switch (action.type) { - case "SET_CONFIG": - return action.config; - default: - return state; - } -} diff --git a/src/redux/reducers/settings.ts b/src/redux/reducers/settings.ts deleted file mode 100644 index 42248faa..00000000 --- a/src/redux/reducers/settings.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { Theme, ThemeOptions } from "../../context/Theme"; - -import { setGlobalEmojiPack } from "../../components/common/Emoji"; - -import type { SyncUpdateAction } from "./sync"; - -type Sounds = "message" | "outbound" | "call_join" | "call_leave"; - -export type SoundOptions = { - [key in Sounds]?: boolean; -}; - -export const DEFAULT_SOUNDS: SoundOptions = { - message: true, - outbound: false, - call_join: true, - call_leave: true, -}; - -export interface NotificationOptions { - desktopEnabled?: boolean; - sounds?: SoundOptions; -} - -export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji"; -export interface AppearanceOptions { - emojiPack?: EmojiPacks; -} - -export interface Settings { - theme?: ThemeOptions; - appearance?: AppearanceOptions; - notification?: NotificationOptions; -} - -export type SettingsAction = - | { type: undefined } - | { - type: "SETTINGS_SET_THEME"; - theme: ThemeOptions; - } - | { - type: "SETTINGS_SET_THEME_OVERRIDE"; - custom?: Partial; - } - | { - type: "SETTINGS_SET_NOTIFICATION_OPTIONS"; - options: NotificationOptions; - } - | { - type: "SETTINGS_SET_APPEARANCE"; - options: Partial; - } - | SyncUpdateAction - | { - type: "RESET"; - }; - -export function settings( - state = {} as Settings, - action: SettingsAction, -): Settings { - // setGlobalEmojiPack(state.appearance?.emojiPack ?? "mutant"); - - switch (action.type) { - case "SETTINGS_SET_THEME": - return { - ...state, - theme: { - ...state.theme, - ...action.theme, - }, - }; - case "SETTINGS_SET_THEME_OVERRIDE": - return { - ...state, - theme: { - ...state.theme, - custom: { - ...state.theme?.custom, - ...action.custom, - }, - }, - }; - case "SETTINGS_SET_NOTIFICATION_OPTIONS": - return { - ...state, - notification: { - ...state.notification, - ...action.options, - }, - }; - case "SETTINGS_SET_APPEARANCE": - return { - ...state, - appearance: { - ...state.appearance, - ...action.options, - }, - }; - case "SYNC_UPDATE": - return { - ...state, - appearance: action.update.appearance?.[1] ?? state.appearance, - theme: action.update.theme?.[1] ?? state.theme, - }; - case "RESET": - return {}; - default: - return state; - } -} diff --git a/src/redux/reducers/sync.ts b/src/redux/reducers/sync.ts deleted file mode 100644 index e62c31c6..00000000 --- a/src/redux/reducers/sync.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Language } from "../../context/Locale"; -import type { ThemeOptions } from "../../context/Theme"; - -import type { Notifications } from "./notifications"; -import type { AppearanceOptions } from "./settings"; - -export type SyncKeys = "theme" | "appearance" | "locale" | "notifications"; - -export interface SyncData { - locale?: Language; - theme?: ThemeOptions; - appearance?: AppearanceOptions; - notifications?: Notifications; -} - -export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [ - "theme", - "appearance", - "locale", - "notifications", -]; -export interface SyncOptions { - disabled?: SyncKeys[]; - revision?: { - [key: string]: number; - }; -} - -export type SyncUpdateAction = { - type: "SYNC_UPDATE"; - update: { [key in SyncKeys]?: [number, SyncData[key]] }; -}; - -export type SyncAction = - | { type: undefined } - | { - type: "SYNC_ENABLE_KEY"; - key: SyncKeys; - } - | { - type: "SYNC_DISABLE_KEY"; - key: SyncKeys; - } - | { - type: "SYNC_SET_REVISION"; - key: SyncKeys; - timestamp: number; - } - | SyncUpdateAction; - -export function sync( - state = {} as SyncOptions, - action: SyncAction, -): SyncOptions { - switch (action.type) { - case "SYNC_DISABLE_KEY": - return { - ...state, - disabled: [ - ...(state.disabled ?? []).filter((v) => v !== action.key), - action.key, - ], - }; - case "SYNC_ENABLE_KEY": - return { - ...state, - disabled: state.disabled?.filter((v) => v !== action.key), - }; - case "SYNC_SET_REVISION": - return { - ...state, - revision: { - ...state.revision, - [action.key]: action.timestamp, - }, - }; - case "SYNC_UPDATE": { - const revision = { ...state.revision }; - for (const key of Object.keys(action.update)) { - const value = action.update[key as SyncKeys]; - if (value) { - revision[key] = value[0]; - } - } - - return { - ...state, - revision, - }; - } - default: - return state; - } -} diff --git a/src/redux/reducers/themes.ts b/src/redux/reducers/themes.ts deleted file mode 100644 index 89b9c840..00000000 --- a/src/redux/reducers/themes.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Theme } from "../../context/Theme"; - -import { ThemeMetadata } from "../../pages/settings/panes/ThemeShop"; - -export interface StoredTheme { - slug: string; - meta: ThemeMetadata; - theme: Theme; -} - -export type Themes = Record; - -export type ThemesAction = - | { type: undefined } - | { type: "THEMES_SET_THEME"; theme: StoredTheme } - | { type: "THEMES_REMOVE_THEME"; slug: string } - | { type: "RESET" }; - -export function themes(state: Themes = {}, action: ThemesAction) { - switch (action.type) { - case "THEMES_SET_THEME": - return { - ...state, - [action.theme.slug]: action.theme, - }; - case "THEMES_REMOVE_THEME": - return { ...state, [action.slug]: null }; - case "RESET": - return {}; - default: - return state; - } -} diff --git a/src/redux/reducers/trusted_links.ts b/src/redux/reducers/trusted_links.ts deleted file mode 100644 index 4675b3cd..00000000 --- a/src/redux/reducers/trusted_links.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface TrustedLinks { - domains?: string[]; -} - -export type TrustedLinksAction = - | { type: undefined } - | { - type: "TRUSTED_LINKS_ADD_DOMAIN"; - domain: string; - } - | { - type: "TRUSTED_LINKS_REMOVE_DOMAIN"; - domain: string; - }; - -export function trustedLinks( - state = {} as TrustedLinks, - action: TrustedLinksAction, -): TrustedLinks { - switch (action.type) { - case "TRUSTED_LINKS_ADD_DOMAIN": - return { - ...state, - domains: [ - ...(state.domains ?? []).filter((v) => v !== action.domain), - action.domain, - ], - }; - case "TRUSTED_LINKS_REMOVE_DOMAIN": - return { - ...state, - domains: state.domains?.filter((v) => v !== action.domain), - }; - default: - return state; - } -} diff --git a/src/redux/reducers/unreads.ts b/src/redux/reducers/unreads.ts deleted file mode 100644 index e7f57886..00000000 --- a/src/redux/reducers/unreads.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ChannelUnread } from "revolt-api/types/Sync"; -import { ulid } from "ulid"; - -export interface Unreads { - [key: string]: Partial>; -} - -export type UnreadsAction = - | { type: undefined } - | { - type: "UNREADS_MARK_READ"; - channel: string; - message: string; - } - | { - type: "UNREADS_MARK_MULTIPLE_READ"; - channels: string[]; - } - | { - type: "UNREADS_SET"; - unreads: ChannelUnread[]; - } - | { - type: "UNREADS_MENTION"; - channel: string; - message: string; - } - | { - type: "RESET"; - }; - -export function unreads(state = {} as Unreads, action: UnreadsAction): Unreads { - switch (action.type) { - case "UNREADS_MARK_READ": - return { - ...state, - [action.channel]: { - last_id: action.message, - }, - }; - case "UNREADS_MARK_MULTIPLE_READ": { - const newState = { ...state }; - const last_id = ulid(); - for (const channel of action.channels) { - newState[channel] = { - last_id, - }; - } - - return newState; - } - case "UNREADS_SET": { - const obj: Unreads = {}; - for (const entry of action.unreads) { - const { _id, ...v } = entry; - obj[_id.channel] = v; - } - - return obj; - } - case "UNREADS_MENTION": { - const obj = state[action.channel]; - - return { - ...state, - [action.channel]: { - ...obj, - mentions: [...(obj?.mentions ?? []), action.message], - }, - }; - } - case "RESET": - return {}; - default: - return state; - } -} diff --git a/yarn.lock b/yarn.lock index 44bc6454..ce022ce1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2463,10 +2463,10 @@ events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -exponential-backoff@^3.1.0: +"exponential-backoff@npm:@insertish/exponential-backoff": version "3.1.0" - resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.0.tgz#9409c7e579131f8bd4b32d7d8094a911040f2e68" - integrity sha512-oBuz5SYz5zzyuHINoe9ooePwSu0xApKWgeNzok4hZ5YKXFh9zrQBEM15CXqoZkJJPuI2ArvqjPQd8UKJA753XA== + resolved "https://registry.yarnpkg.com/@insertish/exponential-backoff/-/exponential-backoff-3.1.0.tgz#1d2e4c215fa8647779cfeab74ecb54a5c36835e6" + integrity sha512-8Jab9OfjheI84T04QjUwXceSO1DMGy8goDqVdnuoffC2fg23zBnikLJkrRHiT/ao4c08v4R2mU7+/DXMWmROng== fake-mediastreamtrack@^1.1.6: version "1.1.6" @@ -2912,10 +2912,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -isomorphic-ws@^4.0.1: +"isomorphic-ws@npm:@insertish/isomorphic-ws": version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + resolved "https://registry.yarnpkg.com/@insertish/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#5bcd6f73b93efa9ccdb6abf887ae808d40827169" + integrity sha512-kFD/p8T4Hkqr992QrdkbW/cQ/W/q2d9MPCobwzBv2PwTKLkCD9RaYDy6m17qRnSLQQ5PU0kHCG8kaOwAqzj1vQ== javascript-natural-sort@0.7.1: version "0.7.1" @@ -3576,18 +3576,6 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^16.13.1" -react-redux@^7.2.4: - version "7.2.4" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" - integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== - dependencies: - "@babel/runtime" "^7.12.1" - "@types/react-redux" "^7.1.16" - hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.13.1" - react-router-dom@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.1.tgz#34af8b551a4ce17487d3f80e651b91651978dff6" @@ -3651,7 +3639,7 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redux@^4.0.0, redux@^4.0.4, redux@^4.1.0: +redux@^4.0.0, redux@^4.0.4: version "4.1.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47" integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw== @@ -3770,15 +3758,15 @@ revolt-api@^0.5.3-alpha.9: resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-alpha.9.tgz#46e75b7d8f9c6702df39039b829dddbb7897f237" integrity sha512-L8K9uPV3ME8bLdtWm8L9iPQvFM0GghA+5LzmWFjd6Gbn56u22ZYub2lABi4iHrWgeA2X41dGSsuSBgHSlts9Og== -revolt.js@5.2.0-patch.0: - version "5.2.0-patch.0" - resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.2.0-patch.0.tgz#af6afc402399e5394b50b2e7d1573ff490fd3906" - integrity sha512-PnHKqRpEvrBFm1xtLA/lGG5FIsp5kW4eB8sYiejjQCA1DWi7Xg6MNvyOjjha6jKftPXF8roivfZWEnM7sY1bnA== +revolt.js@5.2.1-patch.1: + version "5.2.1-patch.1" + resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.2.1-patch.1.tgz#4b392d4dae12ea28f559ef89790368f53788c81d" + integrity sha512-u2vvbCWXKx+vZKqlt5izowf9XnMbWdh3GaPMzipek6l6mBYSCIlr796HoiiIO3c2T3AWqh3zav97rm8z3jOIXg== dependencies: axios "^0.21.4" eventemitter3 "^4.0.7" - exponential-backoff "^3.1.0" - isomorphic-ws "^4.0.1" + exponential-backoff "npm:@insertish/exponential-backoff" + isomorphic-ws "npm:@insertish/isomorphic-ws" lodash.defaultsdeep "^4.6.1" lodash.flatten "^4.4.0" lodash.isequal "^4.5.0"