import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useAtom } from 'jotai'; import { Box, Icon, IconButton, Icons, Text, color, config } from 'folds'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { scheduledMessagesAtom, ScheduledMessage } from '../../state/scheduledMessages'; import { cancelScheduledMessage } from '../../utils/scheduledMessages'; interface ScheduledMessagesTrayProps { roomId: string; } function formatSendAt(sendAt: number): string { const date = new Date(sendAt); const now = new Date(); const isToday = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); const isTomorrow = date.getFullYear() === tomorrow.getFullYear() && date.getMonth() === tomorrow.getMonth() && date.getDate() === tomorrow.getDate(); const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); if (isToday) return `Today ${timeStr}`; if (isTomorrow) return `Tomorrow ${timeStr}`; return `${date.toLocaleDateString()} ${timeStr}`; } export function ScheduledMessagesTray({ roomId }: ScheduledMessagesTrayProps) { const mx = useMatrixClient(); const [scheduledMessages, setScheduledMessages] = useAtom(scheduledMessagesAtom); const [expanded, setExpanded] = useState(false); const [cancelling, setCancelling] = useState>(new Set()); const messages = useMemo(() => scheduledMessages.get(roomId) ?? [], [scheduledMessages, roomId]); // Remove scheduled messages whose time has passed useEffect(() => { if (messages.length === 0) return undefined; const nearestSendAt = Math.min(...messages.map((m) => m.sendAt)); const delay = nearestSendAt - Date.now(); const timer = setTimeout( () => { const now = Date.now(); setScheduledMessages((prev) => { const next = new Map(prev); const current = next.get(roomId) ?? []; const remaining = current.filter((m) => m.sendAt > now); if (remaining.length === 0) { next.delete(roomId); } else { next.set(roomId, remaining); } return next; }); }, Math.max(0, delay) + 2000, ); // 2s grace after scheduled time return () => clearTimeout(timer); }, [messages, roomId, setScheduledMessages]); const handleCancel = useCallback( async (msg: ScheduledMessage) => { if (cancelling.has(msg.delayId)) return; setCancelling((prev) => new Set(prev).add(msg.delayId)); try { await cancelScheduledMessage(mx, msg.delayId); } catch { // If cancellation fails on the server, still remove locally // since the user intends to remove it } finally { setScheduledMessages((prev) => { const next = new Map(prev); const current = next.get(roomId) ?? []; const remaining = current.filter((m) => m.delayId !== msg.delayId); if (remaining.length === 0) { next.delete(roomId); } else { next.set(roomId, remaining); } return next; }); setCancelling((prev) => { const next = new Set(prev); next.delete(msg.delayId); return next; }); } }, [mx, roomId, cancelling, setScheduledMessages], ); if (messages.length === 0) return null; return ( {/* Tray header */} setExpanded((v) => !v)} as="button" aria-expanded={expanded} aria-label={`${messages.length} scheduled message${messages.length !== 1 ? 's' : ''}`} > {messages.length} scheduled message{messages.length !== 1 ? 's' : ''} {/* Tray items */} {expanded && ( {messages.map((msg) => ( {typeof msg.content.body === 'string' ? (msg.content.body as string) : '(message)'} {formatSendAt(msg.sendAt)} { e.stopPropagation(); handleCancel(msg); }} > ))} )} ); }