feat(mobx): add message queue store

This commit is contained in:
Paul 2021-12-12 15:33:47 +00:00
parent ec83230c59
commit faca4ac32b
6 changed files with 118 additions and 76 deletions

View file

@ -116,7 +116,7 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$");
export const CAN_UPLOAD_AT_ONCE = 4; export const CAN_UPLOAD_AT_ONCE = 4;
export default observer(({ channel }: Props) => { export default observer(({ channel }: Props) => {
const drafts = useApplicationState().draft; const state = useApplicationState();
const [uploadState, setUploadState] = useState<UploadState>({ const [uploadState, setUploadState] = useState<UploadState>({
type: "none", type: "none",
@ -149,24 +149,10 @@ export default observer(({ channel }: Props) => {
); );
} }
// Push message content to draft.
const setMessage = useCallback( const setMessage = useCallback(
(content?: string) => { (content?: string) => state.draft.set(channel._id, content),
drafts.set(channel._id, content); [state.draft, channel._id],
if (content) {
dispatch({
type: "SET_DRAFT",
channel: channel._id,
content,
});
} else {
dispatch({
type: "CLEAR_DRAFT",
channel: channel._id,
});
}
},
[drafts, channel._id],
); );
useEffect(() => { useEffect(() => {
@ -184,10 +170,10 @@ export default observer(({ channel }: Props) => {
.join("\n")}\n\n` .join("\n")}\n\n`
: `${content} `; : `${content} `;
if (!drafts.has(channel._id)) { if (!state.draft.has(channel._id)) {
setMessage(text); setMessage(text);
} else { } 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",
append as (...args: unknown[]) => void, append as (...args: unknown[]) => void,
); );
}, [drafts, channel._id, setMessage]); }, [state.draft, channel._id, setMessage]);
/** /**
* Trigger send message. * Trigger send message.
@ -205,7 +191,7 @@ export default observer(({ channel }: Props) => {
if (uploadState.type === "uploading" || uploadState.type === "sending") if (uploadState.type === "uploading" || uploadState.type === "sending")
return; return;
const content = drafts.get(channel._id)?.trim() ?? ""; const content = state.draft.get(channel._id)?.trim() ?? "";
if (uploadState.type === "attached") return sendFile(content); if (uploadState.type === "attached") return sendFile(content);
if (content.length === 0) return; if (content.length === 0) return;
@ -258,18 +244,13 @@ export default observer(({ channel }: Props) => {
} else { } else {
playSound("outbound"); playSound("outbound");
dispatch({ state.queue.add(nonce, channel._id, {
type: "QUEUE_ADD",
nonce,
channel: channel._id,
message: {
_id: nonce, _id: nonce,
channel: channel._id, channel: channel._id,
author: client.user!._id, author: client.user!._id,
content, content,
replies, replies,
},
}); });
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE)); defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
@ -281,11 +262,7 @@ export default observer(({ channel }: Props) => {
replies, replies,
}); });
} catch (error) { } catch (error) {
dispatch({ state.queue.fail(nonce, takeError(error));
type: "QUEUE_FAIL",
error: takeError(error),
nonce,
});
} }
} }
} }
@ -525,7 +502,7 @@ export default observer(({ channel }: Props) => {
id="message" id="message"
maxLength={2000} maxLength={2000}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
value={drafts.get(channel._id) ?? ""} value={state.draft.get(channel._id) ?? ""}
padding="var(--message-box-padding)" padding="var(--message-box-padding)"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Enter") { if (e.ctrlKey && e.key === "Enter") {
@ -535,7 +512,10 @@ export default observer(({ channel }: Props) => {
if (onKeyDown(e)) return; if (onKeyDown(e)) return;
if (e.key === "ArrowUp" && !drafts.has(channel._id)) { if (
e.key === "ArrowUp" &&
!state.draft.has(channel._id)
) {
e.preventDefault(); e.preventDefault();
internalEmit("MessageRenderer", "edit_last"); internalEmit("MessageRenderer", "edit_last");
return; return;

View file

@ -5,7 +5,7 @@ import { Message } from "revolt.js/dist/maps/Messages";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
import { dispatch } from "../../redux"; import { useApplicationState } from "../../mobx/State";
import { connectState } from "../../redux/connector"; import { connectState } from "../../redux/connector";
import { QueuedMessage } from "../../redux/reducers/queue"; import { QueuedMessage } from "../../redux/reducers/queue";
@ -17,22 +17,13 @@ type Props = {
function StateMonitor(props: Props) { function StateMonitor(props: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
const state = useApplicationState();
useEffect(() => {
dispatch({
type: "QUEUE_DROP_ALL",
});
}, []);
useEffect(() => { useEffect(() => {
function add(msg: Message) { function add(msg: Message) {
if (!msg.nonce) return; if (!msg.nonce) return;
if (!props.messages.find((x) => x.id === msg.nonce)) return; if (!props.messages.find((x) => x.id === msg.nonce)) return;
state.queue.remove(msg.nonce);
dispatch({
type: "QUEUE_REMOVE",
nonce: msg.nonce,
});
} }
client.addListener("message", add); client.addListener("message", add);

View file

@ -32,6 +32,7 @@ import {
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { useApplicationState } from "../mobx/State";
import { dispatch } from "../redux"; import { dispatch } from "../redux";
import { connectState } from "../redux/connector"; import { connectState } from "../redux/connector";
import { import {
@ -141,6 +142,7 @@ export default function ContextMenus() {
const userId = client.user!._id; const userId = client.user!._id;
const status = useContext(StatusContext); const status = useContext(StatusContext);
const isOnline = status === ClientStatus.ONLINE; const isOnline = status === ClientStatus.ONLINE;
const state = useApplicationState();
const history = useHistory(); const history = useHistory();
function contextClick(data?: Action) { function contextClick(data?: Action) {
@ -196,11 +198,7 @@ export default function ContextMenus() {
{ {
const nonce = data.message.id; const nonce = data.message.id;
const fail = (error: string) => const fail = (error: string) =>
dispatch({ state.queue.fail(nonce, error);
type: "QUEUE_FAIL",
nonce,
error,
});
client.channels client.channels
.get(data.message.channel)! .get(data.message.channel)!
@ -211,19 +209,13 @@ export default function ContextMenus() {
}) })
.catch(fail); .catch(fail);
dispatch({ state.queue.start(nonce);
type: "QUEUE_START",
nonce,
});
} }
break; break;
case "cancel_message": case "cancel_message":
{ {
dispatch({ state.queue.remove(data.message.id);
type: "QUEUE_REMOVE",
nonce: data.message.id,
});
} }
break; break;

View file

@ -10,6 +10,7 @@ import Draft from "./stores/Draft";
import Experiments from "./stores/Experiments"; import Experiments from "./stores/Experiments";
import Layout from "./stores/Layout"; import Layout from "./stores/Layout";
import LocaleOptions from "./stores/LocaleOptions"; import LocaleOptions from "./stores/LocaleOptions";
import MessageQueue from "./stores/MessageQueue";
import NotificationOptions from "./stores/NotificationOptions"; import NotificationOptions from "./stores/NotificationOptions";
import ServerConfig from "./stores/ServerConfig"; import ServerConfig from "./stores/ServerConfig";
@ -24,6 +25,7 @@ export default class State {
layout: Layout; layout: Layout;
config: ServerConfig; config: ServerConfig;
notifications: NotificationOptions; notifications: NotificationOptions;
queue: MessageQueue;
private persistent: [string, Persistent<unknown>][] = []; private persistent: [string, Persistent<unknown>][] = [];
@ -38,6 +40,7 @@ export default class State {
this.layout = new Layout(); this.layout = new Layout();
this.config = new ServerConfig(); this.config = new ServerConfig();
this.notifications = new NotificationOptions(); this.notifications = new NotificationOptions();
this.queue = new MessageQueue();
makeAutoObservable(this); makeAutoObservable(this);
this.registerListeners = this.registerListeners.bind(this); this.registerListeners = this.registerListeners.bind(this);

View file

@ -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<QueuedMessage>;
/**
* 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);
}
}

View file

@ -16,6 +16,7 @@ import { useEffect, useState } from "preact/hooks";
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter"; import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
import { ChannelRenderer } from "../../../lib/renderer/Singleton"; import { ChannelRenderer } from "../../../lib/renderer/Singleton";
import { useApplicationState } from "../../../mobx/State";
import { connectState } from "../../../redux/connector"; import { connectState } from "../../../redux/connector";
import { QueuedMessage } from "../../../redux/reducers/queue"; import { QueuedMessage } from "../../../redux/reducers/queue";
@ -33,7 +34,6 @@ import MessageEditor from "./MessageEditor";
interface Props { interface Props {
highlight?: string; highlight?: string;
queue: QueuedMessage[];
renderer: ChannelRenderer; 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 client = useClient();
const userId = client.user!._id; const userId = client.user!._id;
const queue = useApplicationState().queue;
const [editing, setEditing] = useState<string | undefined>(undefined); const [editing, setEditing] = useState<string | undefined>(undefined);
const stopEditing = () => { const stopEditing = () => {
@ -192,8 +193,7 @@ const MessageRenderer = observer(({ renderer, queue, highlight }: Props) => {
const nonces = renderer.messages.map((x) => x.nonce); const nonces = renderer.messages.map((x) => x.nonce);
if (renderer.atBottom) { if (renderer.atBottom) {
for (const msg of queue) { for (const msg of queue.get(renderer.channel._id)) {
if (msg.channel !== renderer.channel._id) continue;
if (nonces.includes(msg.id)) continue; if (nonces.includes(msg.id)) continue;
if (previous) { if (previous) {
@ -237,11 +237,3 @@ const MessageRenderer = observer(({ renderer, queue, highlight }: Props) => {
return <>{render}</>; return <>{render}</>;
}); });
export default memo(
connectState<Omit<Props, "queue">>(MessageRenderer, (state) => {
return {
queue: state.queue,
};
}),
);