import React, { FormEventHandler, useCallback, useEffect, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Box, Button, Header, Icon, IconButton, Icons, Overlay, OverlayBackdrop, OverlayCenter, Spinner, Text, color, config, } from 'folds'; import { IContent } from 'matrix-js-sdk'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { stopPropagation } from '../../utils/keyboard'; import { scheduleMessage } from '../../utils/scheduledMessages'; interface ScheduleMessageModalProps { roomId: string; /** Pre-fill the message body from the composer. Pass null/undefined to open blank. */ initialBody?: string; onScheduled: (delayId: string, sendAt: number, content: IContent) => void; onClose: () => void; } function formatRelativeTime(ms: number): string { const totalSeconds = Math.floor(ms / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); if (hours > 0 && minutes > 0) return `in ${hours}h ${minutes}m`; if (hours > 0) return `in ${hours}h`; if (minutes > 0) return `in ${minutes}m`; return 'in less than a minute'; } function formatSendAt(sendAt: Date): string { const now = new Date(); const isToday = sendAt.getFullYear() === now.getFullYear() && sendAt.getMonth() === now.getMonth() && sendAt.getDate() === now.getDate(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); const isTomorrow = sendAt.getFullYear() === tomorrow.getFullYear() && sendAt.getMonth() === tomorrow.getMonth() && sendAt.getDate() === tomorrow.getDate(); const timeStr = sendAt.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); if (isToday) return `Today at ${timeStr}`; if (isTomorrow) return `Tomorrow at ${timeStr}`; return `${sendAt.toLocaleDateString()} at ${timeStr}`; } function toLocalDatetimeValue(date: Date): string { const pad = (n: number) => String(n).padStart(2, '0'); return ( `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` + `T${pad(date.getHours())}:${pad(date.getMinutes())}` ); } export function ScheduleMessageModal({ roomId, initialBody, onScheduled, onClose, }: ScheduleMessageModalProps) { const mx = useMatrixClient(); const [messageText, setMessageText] = useState(initialBody ?? ''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); // Default: 1 hour from now, rounded to nearest 5 minutes const defaultDate = () => { const d = new Date(Date.now() + 60 * 60 * 1000); d.setSeconds(0, 0); d.setMinutes(Math.ceil(d.getMinutes() / 5) * 5); return d; }; const [datetimeValue, setDatetimeValue] = useState(() => toLocalDatetimeValue(defaultDate()), ); const [preview, setPreview] = useState<{ label: string; relative: string } | null>(null); const updatePreview = useCallback((value: string) => { if (!value) { setPreview(null); return; } const sendAt = new Date(value); const now = Date.now(); const diffMs = sendAt.getTime() - now; if (Number.isNaN(sendAt.getTime()) || diffMs < 60_000) { setPreview(null); return; } setPreview({ label: formatSendAt(sendAt), relative: formatRelativeTime(diffMs) }); }, []); useEffect(() => { updatePreview(datetimeValue); }, [datetimeValue, updatePreview]); const handleSubmit: FormEventHandler = async (e) => { e.preventDefault(); if (submitting) return; if (!datetimeValue) { setError('Please select a date and time.'); return; } const sendAt = new Date(datetimeValue); if (Number.isNaN(sendAt.getTime())) { setError('Invalid date/time.'); return; } const diffMs = sendAt.getTime() - Date.now(); if (diffMs < 60_000) { setError('Scheduled time must be at least 1 minute in the future.'); return; } if (!messageText.trim()) { setError('Please enter a message to schedule.'); return; } const content: IContent = { body: messageText.trim(), msgtype: 'm.text' }; setError(null); setSubmitting(true); try { const delayId = await scheduleMessage(mx, roomId, content, sendAt.getTime()); onScheduled(delayId, sendAt.getTime(), content); onClose(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to schedule message.'); setSubmitting(false); } }; return ( }> {/* Header */}
Schedule Message
{/* Body */} {/* Message input */} Message