diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 8b4d5b15..91b823a2 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -116,7 +116,7 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$"); export const CAN_UPLOAD_AT_ONCE = 4; export default observer(({ channel }: Props) => { - const drafts = useApplicationState().draft; + const state = useApplicationState(); const [uploadState, setUploadState] = useState({ type: "none", @@ -149,24 +149,10 @@ export default observer(({ channel }: Props) => { ); } + // Push message content to draft. const setMessage = useCallback( - (content?: string) => { - drafts.set(channel._id, content); - - if (content) { - dispatch({ - type: "SET_DRAFT", - channel: channel._id, - content, - }); - } else { - dispatch({ - type: "CLEAR_DRAFT", - channel: channel._id, - }); - } - }, - [drafts, channel._id], + (content?: string) => state.draft.set(channel._id, content), + [state.draft, channel._id], ); useEffect(() => { @@ -184,10 +170,10 @@ export default observer(({ channel }: Props) => { .join("\n")}\n\n` : `${content} `; - if (!drafts.has(channel._id)) { + if (!state.draft.has(channel._id)) { setMessage(text); } else { - setMessage(`${drafts.get(channel._id)}\n${text}`); + setMessage(`${state.draft.get(channel._id)}\n${text}`); } } @@ -196,7 +182,7 @@ export default observer(({ channel }: Props) => { "append", append as (...args: unknown[]) => void, ); - }, [drafts, channel._id, setMessage]); + }, [state.draft, channel._id, setMessage]); /** * Trigger send message. @@ -205,7 +191,7 @@ export default observer(({ channel }: Props) => { if (uploadState.type === "uploading" || uploadState.type === "sending") return; - const content = drafts.get(channel._id)?.trim() ?? ""; + const content = state.draft.get(channel._id)?.trim() ?? ""; if (uploadState.type === "attached") return sendFile(content); if (content.length === 0) return; @@ -258,18 +244,13 @@ export default observer(({ channel }: Props) => { } else { playSound("outbound"); - dispatch({ - type: "QUEUE_ADD", - nonce, + state.queue.add(nonce, channel._id, { + _id: nonce, channel: channel._id, - message: { - _id: nonce, - channel: channel._id, - author: client.user!._id, + author: client.user!._id, - content, - replies, - }, + content, + replies, }); defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE)); @@ -281,11 +262,7 @@ export default observer(({ channel }: Props) => { replies, }); } catch (error) { - dispatch({ - type: "QUEUE_FAIL", - error: takeError(error), - nonce, - }); + state.queue.fail(nonce, takeError(error)); } } } @@ -525,7 +502,7 @@ export default observer(({ channel }: Props) => { id="message" maxLength={2000} onKeyUp={onKeyUp} - value={drafts.get(channel._id) ?? ""} + value={state.draft.get(channel._id) ?? ""} padding="var(--message-box-padding)" onKeyDown={(e) => { if (e.ctrlKey && e.key === "Enter") { @@ -535,7 +512,10 @@ export default observer(({ channel }: Props) => { if (onKeyDown(e)) return; - if (e.key === "ArrowUp" && !drafts.has(channel._id)) { + if ( + e.key === "ArrowUp" && + !state.draft.has(channel._id) + ) { e.preventDefault(); internalEmit("MessageRenderer", "edit_last"); return; diff --git a/src/context/revoltjs/StateMonitor.tsx b/src/context/revoltjs/StateMonitor.tsx index 038ea706..757e3c66 100644 --- a/src/context/revoltjs/StateMonitor.tsx +++ b/src/context/revoltjs/StateMonitor.tsx @@ -5,7 +5,7 @@ import { Message } from "revolt.js/dist/maps/Messages"; import { useContext, useEffect } from "preact/hooks"; -import { dispatch } from "../../redux"; +import { useApplicationState } from "../../mobx/State"; import { connectState } from "../../redux/connector"; import { QueuedMessage } from "../../redux/reducers/queue"; @@ -17,22 +17,13 @@ type Props = { function StateMonitor(props: Props) { const client = useContext(AppContext); - - useEffect(() => { - dispatch({ - type: "QUEUE_DROP_ALL", - }); - }, []); + const state = useApplicationState(); useEffect(() => { function add(msg: Message) { if (!msg.nonce) return; if (!props.messages.find((x) => x.id === msg.nonce)) return; - - dispatch({ - type: "QUEUE_REMOVE", - nonce: msg.nonce, - }); + state.queue.remove(msg.nonce); } client.addListener("message", add); diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index eb6e67d1..0b48eab7 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -32,6 +32,7 @@ import { 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 { @@ -141,6 +142,7 @@ export default function ContextMenus() { const userId = client.user!._id; const status = useContext(StatusContext); const isOnline = status === ClientStatus.ONLINE; + const state = useApplicationState(); const history = useHistory(); function contextClick(data?: Action) { @@ -196,11 +198,7 @@ export default function ContextMenus() { { const nonce = data.message.id; const fail = (error: string) => - dispatch({ - type: "QUEUE_FAIL", - nonce, - error, - }); + state.queue.fail(nonce, error); client.channels .get(data.message.channel)! @@ -211,19 +209,13 @@ export default function ContextMenus() { }) .catch(fail); - dispatch({ - type: "QUEUE_START", - nonce, - }); + state.queue.start(nonce); } break; case "cancel_message": { - dispatch({ - type: "QUEUE_REMOVE", - nonce: data.message.id, - }); + state.queue.remove(data.message.id); } break; diff --git a/src/mobx/State.ts b/src/mobx/State.ts index aff12eb8..041ebb2e 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -10,6 +10,7 @@ import Draft from "./stores/Draft"; import Experiments from "./stores/Experiments"; import Layout from "./stores/Layout"; import LocaleOptions from "./stores/LocaleOptions"; +import MessageQueue from "./stores/MessageQueue"; import NotificationOptions from "./stores/NotificationOptions"; import ServerConfig from "./stores/ServerConfig"; @@ -24,6 +25,7 @@ export default class State { layout: Layout; config: ServerConfig; notifications: NotificationOptions; + queue: MessageQueue; private persistent: [string, Persistent][] = []; @@ -38,6 +40,7 @@ export default class State { this.layout = new Layout(); this.config = new ServerConfig(); this.notifications = new NotificationOptions(); + this.queue = new MessageQueue(); makeAutoObservable(this); this.registerListeners = this.registerListeners.bind(this); diff --git a/src/mobx/stores/MessageQueue.ts b/src/mobx/stores/MessageQueue.ts index e69de29b..c1505435 100644 --- a/src/mobx/stores/MessageQueue.ts +++ b/src/mobx/stores/MessageQueue.ts @@ -0,0 +1,84 @@ +import { + action, + computed, + IObservableArray, + makeAutoObservable, + observable, +} from "mobx"; + +import Store from "../interfaces/Store"; + +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; +} + +/** + * Handles waiting for messages to send and send failure. + */ +export default class MessageQueue implements Store { + private messages: IObservableArray; + + /** + * Construct new MessageQueue store. + */ + constructor() { + this.messages = observable.array([]); + makeAutoObservable(this); + } + + get id() { + return "queue"; + } + + @action add(id: string, channel: string, data: QueuedMessageData) { + this.messages.push({ + id, + channel, + data, + status: QueueStatus.SENDING, + }); + } + + @action fail(id: string, error: string) { + const entry = this.messages.find((x) => x.id === id)!; + entry.status = QueueStatus.ERRORED; + entry.error = error; + } + + @action start(id: string) { + const entry = this.messages.find((x) => x.id === id)!; + entry.status = QueueStatus.SENDING; + } + + @action remove(id: string) { + const entry = this.messages.find((x) => x.id === id)!; + this.messages.remove(entry); + } + + @computed get(channel: string) { + return this.messages.filter((x) => x.channel === channel); + } +} diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index 270cae70..47f0f1ae 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -16,6 +16,7 @@ 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"; @@ -33,7 +34,6 @@ import MessageEditor from "./MessageEditor"; interface Props { highlight?: string; - queue: QueuedMessage[]; renderer: ChannelRenderer; } @@ -48,9 +48,10 @@ const BlockedMessage = styled.div` } `; -const MessageRenderer = observer(({ renderer, queue, highlight }: Props) => { +export default observer(({ renderer, highlight }: Props) => { const client = useClient(); const userId = client.user!._id; + const queue = useApplicationState().queue; const [editing, setEditing] = useState(undefined); const stopEditing = () => { @@ -192,8 +193,7 @@ const MessageRenderer = observer(({ renderer, queue, highlight }: Props) => { const nonces = renderer.messages.map((x) => x.nonce); if (renderer.atBottom) { - for (const msg of queue) { - if (msg.channel !== renderer.channel._id) continue; + for (const msg of queue.get(renderer.channel._id)) { if (nonces.includes(msg.id)) continue; if (previous) { @@ -237,11 +237,3 @@ const MessageRenderer = observer(({ renderer, queue, highlight }: Props) => { return <>{render}; }); - -export default memo( - connectState>(MessageRenderer, (state) => { - return { - queue: state.queue, - }; - }), -);