1import { Atom } from "effect/reactivity";
2
3import { appAtomRegistry } from "./atom-registry";
4
5interface ThreadComposerError {
6 readonly message: string;
7 /** The queued message this error is about, when it is about one. */
8 readonly messageId: string | null;
9}
10
11/**
12 * Why a thread's last message did not go out, shown above that thread's
13 * composer. The outbox drain can reject a message after the user has left the
14 * thread, so the reason is kept per thread until they dismiss it, send again,
15 * or the message it describes is delivered after all. Keyed by `scopedThreadKey`.
16 */
17export const threadComposerErrorsAtom = Atom.make<Readonly<Record<string, ThreadComposerError>>>(
18 {},
19).pipe(Atom.keepAlive, Atom.withLabel("mobile:thread-composer-errors"));
20
21export function setThreadComposerError(
22 threadKey: string,
23 message: string,
24 messageId: string | null = null,
25): void {
26 appAtomRegistry.set(threadComposerErrorsAtom, {
27 ...appAtomRegistry.get(threadComposerErrorsAtom),
28 [threadKey]: { message, messageId },
29 });
30}
31
32/** With `messageId`, clears only an error that describes that message. */
33export function clearThreadComposerError(threadKey: string, messageId?: string): void {
34 const current = appAtomRegistry.get(threadComposerErrorsAtom);
35 const error = current[threadKey];
36 if (!error || (messageId !== undefined && error.messageId !== messageId)) {
37 return;
38 }
39 const next = { ...current };
40 delete next[threadKey];
41 appAtomRegistry.set(threadComposerErrorsAtom, next);
42}
43
44export function clearThreadComposerErrorsForEnvironment(environmentId: string): void {
45 const current = appAtomRegistry.get(threadComposerErrorsAtom);
46 const prefix = `${environmentId}:`;
47 const keys = Object.keys(current).filter((key) => key.startsWith(prefix));
48 if (keys.length === 0) {
49 return;
50 }
51 const next = { ...current };
52 for (const key of keys) {
53 delete next[key];
54 }
55 appAtomRegistry.set(threadComposerErrorsAtom, next);
56}