feat(reminders): view and cancel a message's existing reminders
There was no way to see or cancel a reminder once set (removeReminder was only called by the fire-and-forget monitor), and addReminder didn't dedupe, so a message could silently accumulate duplicate reminders. The Remind Me dialog now lists the reminders already set on that message (soonest first) each with a cancel button. - New shared, tested formatFriendlyDateTime(ts, now?) in utils/datetimeInput.ts (Today/Tomorrow/date + time). - Per-row cancel busy-guard; inline "Could not cancel" on failure. Also applies two nits from the custom-time review: focus the date input when the custom picker is revealed, and clear the error when editing date/time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null>(null);
|
||||
const [customOpen, setCustomOpen] = useState(false);
|
||||
const [cancelling, setCancelling] = useState<Set<number>>(new Set());
|
||||
const def = useMemo(() => defaultCustomDate(), []);
|
||||
const [dateValue, setDateValue] = useState(() => toLocalDate(def));
|
||||
const [timeValue, setTimeValue] = useState(() => toLocalTime(def));
|
||||
const dateInputRef = useRef<HTMLInputElement>(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
|
||||
<Line size="300" />
|
||||
</>
|
||||
)}
|
||||
{existing.length > 0 && (
|
||||
<>
|
||||
<Box
|
||||
direction="Column"
|
||||
gap="100"
|
||||
style={{ padding: `${config.space.S200} ${config.space.S200} 0` }}
|
||||
>
|
||||
<Text size="L400" priority="300" style={{ paddingLeft: config.space.S200 }}>
|
||||
{existing.length === 1 ? 'Reminder set' : 'Reminders set'}
|
||||
</Text>
|
||||
{existing.map((r) => (
|
||||
<Box key={r.timestamp} alignItems="Center" gap="200">
|
||||
<Icon src={Icons.Clock} size="100" style={{ flexShrink: 0 }} />
|
||||
<Text size="T200" style={{ flexGrow: 1, minWidth: 0 }} truncate>
|
||||
{formatFriendlyDateTime(r.timestamp)}
|
||||
</Text>
|
||||
<IconButton
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="SurfaceVariant"
|
||||
fill="None"
|
||||
disabled={cancelling.has(r.timestamp)}
|
||||
onClick={() => handleCancelExisting(r.timestamp)}
|
||||
aria-label={`Cancel reminder for ${formatFriendlyDateTime(r.timestamp)}`}
|
||||
>
|
||||
<Icon src={Icons.Cross} size="100" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Line size="300" style={{ marginTop: config.space.S200 }} />
|
||||
</>
|
||||
)}
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{presets.map((p) => (
|
||||
<Button
|
||||
@@ -171,12 +239,16 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
|
||||
Date
|
||||
</Text>
|
||||
<input
|
||||
ref={dateInputRef}
|
||||
id="remind-date"
|
||||
type="date"
|
||||
value={dateValue}
|
||||
min={toLocalDate(new Date())}
|
||||
disabled={busy}
|
||||
onChange={(e) => setDateValue(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setDateValue(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
style={pickerInputStyle(color, config)}
|
||||
/>
|
||||
</Box>
|
||||
@@ -189,7 +261,10 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
|
||||
type="time"
|
||||
value={timeValue}
|
||||
disabled={busy}
|
||||
onChange={(e) => setTimeValue(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setTimeValue(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
style={pickerInputStyle(color, config)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user