diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index 4b91bfe0c..91a008cc9 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -750,6 +750,7 @@ Redacted events display "This message has been deleted" along with the redaction - Message context menu → **Remind Me** sets a personal reminder to revisit a message; reminders are stored in `io.lotus.reminders` account data (sync across devices) via `useReminders`, and fire from `ClientNonUIFeatures`. - `RemindMeDialog.tsx` offers quick presets (in 20 min / 1 hour / 3 hours / tomorrow 9am) **plus a "Custom time…" option** that reveals date + time pickers for an arbitrary reminder time (validated to be ≥ 1 minute in the future). +- **Manage existing reminders**: opening the dialog on a message that already has reminders lists them (soonest first, friendly time via `formatFriendlyDateTime`) each with a cancel (×) button, so you can see and remove pending reminders instead of silently stacking duplicates. - The date/time input helpers (`toLocalDate`, `toLocalTime`, `parseLocalDateTime`, `pickerInputStyle`) are shared, pure, and unit-tested in `src/app/utils/datetimeInput.ts` (`datetimeInput.test.ts`) — also used by `ScheduleMessageModal` (deduped from a prior inline copy). ### File Upload Compression (opt-in) diff --git a/src/app/features/room/message/RemindMeDialog.tsx b/src/app/features/room/message/RemindMeDialog.tsx index dab5dd855..b9216c117 100644 --- a/src/app/features/room/message/RemindMeDialog.tsx +++ b/src/app/features/room/message/RemindMeDialog.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Box, @@ -24,6 +24,7 @@ import { toLocalTime, parseLocalDateTime, pickerInputStyle, + formatFriendlyDateTime, } from '../../../utils/datetimeInput'; type RemindMeDialogProps = { @@ -56,14 +57,48 @@ function defaultCustomDate(): Date { export function RemindMeDialog({ roomId, eventId, previewText, onClose }: RemindMeDialogProps) { const modalStyle = useModalStyle(320); - const { addReminder } = useReminders(); + const { addReminder, removeReminder, reminders } = useReminders(); const presets = useMemo(() => getPresets(), []); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [customOpen, setCustomOpen] = useState(false); + const [cancelling, setCancelling] = useState>(new Set()); const def = useMemo(() => defaultCustomDate(), []); const [dateValue, setDateValue] = useState(() => toLocalDate(def)); const [timeValue, setTimeValue] = useState(() => toLocalTime(def)); + const dateInputRef = useRef(null); + + // Reminders already set on this message (soonest first) — so the user can see + // and cancel them instead of silently stacking duplicates. + const existing = useMemo( + () => + reminders + .filter((r) => r.eventId === eventId) + .sort((a, b) => a.timestamp - b.timestamp), + [reminders, eventId], + ); + + // Move focus into the revealed date input for keyboard/SR users. + useEffect(() => { + if (customOpen) dateInputRef.current?.focus(); + }, [customOpen]); + + const handleCancelExisting = async (timestamp: number) => { + if (cancelling.has(timestamp)) return; + setCancelling((prev) => new Set(prev).add(timestamp)); + setError(null); + try { + await removeReminder(eventId, timestamp); + } catch { + setError('Could not cancel reminder. Try again.'); + } finally { + setCancelling((prev) => { + const next = new Set(prev); + next.delete(timestamp); + return next; + }); + } + }; const commit = async (timestamp: number) => { if (busy) return; @@ -146,6 +181,39 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind )} + {existing.length > 0 && ( + <> + + + {existing.length === 1 ? 'Reminder set' : 'Reminders set'} + + {existing.map((r) => ( + + + + {formatFriendlyDateTime(r.timestamp)} + + handleCancelExisting(r.timestamp)} + aria-label={`Cancel reminder for ${formatFriendlyDateTime(r.timestamp)}`} + > + + + + ))} + + + + )} {presets.map((p) => (