2026-06-28 09:17:19 -04:00
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
2026-06-17 20:26:43 -04:00
|
|
|
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));
|
|
|
|
|
|
2026-06-28 09:17:19 -04:00
|
|
|
// 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);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-06-17 20:26:43 -04:00
|
|
|
useAccountDataCallback(
|
|
|
|
|
mx,
|
|
|
|
|
useCallback(
|
|
|
|
|
(evt) => {
|
|
|
|
|
if (evt.getType() === REMINDERS_KEY) {
|
2026-06-28 09:17:19 -04:00
|
|
|
applyServerState(evt.getContent<RemindersContent>()?.reminders ?? []);
|
2026-06-17 20:26:43 -04:00
|
|
|
}
|
|
|
|
|
},
|
2026-06-28 09:17:19 -04:00
|
|
|
[applyServerState],
|
2026-06-17 20:26:43 -04:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Re-read on mx change
|
|
|
|
|
useEffect(() => {
|
2026-06-28 09:17:19 -04:00
|
|
|
applyServerState(readReminders(mx));
|
|
|
|
|
}, [mx, applyServerState]);
|
2026-06-17 20:26:43 -04:00
|
|
|
|
2026-06-28 09:17:19 -04:00
|
|
|
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;
|
2026-06-17 20:26:43 -04:00
|
|
|
},
|
|
|
|
|
[mx],
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-28 09:17:19 -04:00
|
|
|
const addReminder = useCallback(
|
|
|
|
|
(r: Reminder) => enqueueWrite((current) => [...current, r]),
|
|
|
|
|
[enqueueWrite],
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-17 20:26:43 -04:00
|
|
|
const removeReminder = useCallback(
|
2026-06-28 09:17:19 -04:00
|
|
|
(eventId: string, timestamp: number) =>
|
|
|
|
|
enqueueWrite((current) =>
|
|
|
|
|
current.filter((r) => !(r.eventId === eventId && r.timestamp === timestamp)),
|
|
|
|
|
),
|
|
|
|
|
[enqueueWrite],
|
2026-06-17 20:26:43 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getReminders = useCallback(() => reminders, [reminders]);
|
|
|
|
|
|
|
|
|
|
return { reminders, addReminder, removeReminder, getReminders };
|
|
|
|
|
}
|