import { useCallback, useEffect } from 'react'; import { MatrixClient } from 'matrix-js-sdk'; import { useMatrixClient } from './useMatrixClient'; import { createAccountDataListStore } from './createAccountDataListStore'; export type Reminder = { roomId: string; eventId: string; timestamp: number; // Message preview. Account data is stored UNENCRYPTED on the homeserver, so // this is omitted for E2EE rooms (see stripReminderText) and the toast resolves // the text locally at fire time. Optional for entries stored before that policy. message?: string; }; const REMINDERS_KEY = 'io.lotus.reminders'; type RemindersContent = { reminders: Reminder[]; }; /** Reduce a reminder to its non-text fields ({roomId, eventId, timestamp}). */ export const stripReminderText = (r: Reminder): Reminder => ({ roomId: r.roomId, eventId: r.eventId, timestamp: r.timestamp, }); /** Apply the E2EE storage policy to a reminder about to be persisted. */ export const toStorableReminder = ( r: Reminder, isEncryptedRoom: (roomId: string) => boolean, ): Reminder => (isEncryptedRoom(r.roomId) ? stripReminderText(r) : r); /** * One-time cleanup for entries persisted before the E2EE policy; `undefined` * when nothing needs to change (so callers can skip the write). */ export const cleanupEncryptedReminders = ( reminders: Reminder[], isEncryptedRoom: (roomId: string) => boolean, ): Reminder[] | undefined => { let changed = false; const next = reminders.map((r) => { if (r.message !== undefined && isEncryptedRoom(r.roomId)) { changed = true; return stripReminderText(r); } return r; }); return changed ? next : undefined; }; const isEncryptedRoomFor = (mx: MatrixClient) => (roomId: string): boolean => mx.getRoom(roomId)?.hasEncryptionStateEvent() ?? false; // Shared, concurrency-safe store. See createAccountDataListStore for why the // snapshot + write queue must be module-scoped (writes are serialized to avoid // lost updates, since setAccountData replaces the whole content with no merge). const remindersStore = createAccountDataListStore({ eventType: REMINDERS_KEY, read: (content) => content?.reminders ?? [], write: (reminders) => ({ reminders }), }); // Once-per-client guard for the load-time cleanup (mirrors useBookmarks). let cleanedUpFor: MatrixClient | null = null; export function useReminders(): { reminders: Reminder[]; addReminder: (r: Reminder) => Promise; removeReminder: (eventId: string, timestamp: number) => Promise; getReminders: () => Reminder[]; } { const mx = useMatrixClient(); const reminders = remindersStore.useValue(mx); // Strip text from entries saved (by older builds) for rooms that are encrypted, // writing the store back at most once per client. useEffect(() => { if (cleanedUpFor === mx) return; cleanedUpFor = mx; if (cleanupEncryptedReminders(remindersStore.getLatest(mx), isEncryptedRoomFor(mx))) { remindersStore .enqueueWrite( mx, (current) => cleanupEncryptedReminders(current, isEncryptedRoomFor(mx)) ?? current, ) .catch(() => { if (cleanedUpFor === mx) cleanedUpFor = null; }); } }, [mx]); const addReminder = useCallback( (r: Reminder) => remindersStore.enqueueWrite(mx, (current) => [ ...current, // Never upload plaintext for E2EE rooms — account data is server-readable. toStorableReminder(r, isEncryptedRoomFor(mx)), ]), [mx], ); const removeReminder = useCallback( (eventId: string, timestamp: number) => remindersStore.enqueueWrite(mx, (current) => current.filter((r) => !(r.eventId === eventId && r.timestamp === timestamp)), ), [mx], ); const getReminders = useCallback(() => remindersStore.getLatest(mx), [mx]); return { reminders, addReminder, removeReminder, getReminders }; }