Files
cinny/src/app/hooks/useReminders.ts
T

99 lines
3.1 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useRef, useState } from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
import { useAccountDataCallback } from './useAccountDataCallback';
export type Reminder = {
roomId: string;
eventId: string;
timestamp: number;
message: string;
};
const REMINDERS_KEY = 'io.lotus.reminders';
type RemindersContent = {
reminders: Reminder[];
};
function readReminders(mx: MatrixClient): Reminder[] {
return (
(mx.getAccountData(REMINDERS_KEY as any)?.getContent() as RemindersContent | undefined)
?.reminders ?? []
);
}
export function useReminders(): {
reminders: Reminder[];
addReminder: (r: Reminder) => Promise<void>;
removeReminder: (eventId: string, timestamp: number) => Promise<void>;
getReminders: () => Reminder[];
} {
const mx = useMatrixClient();
const [reminders, setReminders] = useState<Reminder[]>(() => readReminders(mx));
// Authoritative local snapshot used to compute mutations. Reading
// mx.getAccountData() per-mutation is racy: two quick add/remove calls both
// read the same stale baseline and the second write clobbers the first
// (N113). We instead mutate from this ref, kept in sync with server echoes.
const latestRef = useRef<Reminder[]>(reminders);
// Serialize writes so overlapping setAccountData calls can't land out of
// order on the server (last-write-wins would otherwise drop data).
const writeQueueRef = useRef<Promise<unknown>>(Promise.resolve());
const applyServerState = useCallback((list: Reminder[]) => {
latestRef.current = list;
setReminders(list);
}, []);
useAccountDataCallback(
mx,
useCallback(
(evt) => {
if (evt.getType() === REMINDERS_KEY) {
applyServerState(evt.getContent<RemindersContent>()?.reminders ?? []);
}
},
[applyServerState],
),
);
// Re-read on mx change
useEffect(() => {
applyServerState(readReminders(mx));
}, [mx, applyServerState]);
const enqueueWrite = useCallback(
(compute: (current: Reminder[]) => Reminder[]): Promise<void> => {
const run = writeQueueRef.current.then(async () => {
const next = compute(latestRef.current);
latestRef.current = next;
setReminders(next);
await (mx as any).setAccountData(REMINDERS_KEY, { reminders: next });
});
// Keep the chain alive even if one write rejects, but propagate the
// rejection to this caller so it can react (e.g. retry).
writeQueueRef.current = run.catch(() => undefined);
return run;
},
[mx],
);
const addReminder = useCallback(
(r: Reminder) => enqueueWrite((current) => [...current, r]),
[enqueueWrite],
);
const removeReminder = useCallback(
(eventId: string, timestamp: number) =>
enqueueWrite((current) =>
current.filter((r) => !(r.eventId === eventId && r.timestamp === timestamp)),
),
[enqueueWrite],
);
const getReminders = useCallback(() => reminders, [reminders]);
return { reminders, addReminder, removeReminder, getReminders };
}