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 toLocalDate(date: Date): string { const pad = (n: number) => String(n).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } function toLocalTime(date: Date): string { const pad = (n: number) => String(n).padStart(2, '0'); return `${pad(date.getHours())}:${pad(date.getMinutes())}`; } // Shared style for date/time inputs — dark-mode calendar/clock popup via colorScheme. const pickerInputStyle = (c: typeof color, cfg: typeof config): React.CSSProperties => ({ background: c.SurfaceVariant.Container, color: c.SurfaceVariant.OnContainer, border: `${cfg.borderWidth.B300} solid ${c.SurfaceVariant.ContainerLine}`, borderRadius: cfg.radii.R300, padding: `${cfg.space.S200} ${cfg.space.S300}`, fontSize: '0.875rem', width: '100%', boxSizing: 'border-box', outline: 'none', fontFamily: 'inherit', // Hint browser to render the calendar/clock popup in dark mode colorScheme: 'dark', }); 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 def = defaultDate(); const [dateValue, setDateValue] = useState(() => toLocalDate(def)); const [timeValue, setTimeValue] = useState(() => toLocalTime(def)); const getSendAt = useCallback((): Date | null => { if (!dateValue || !timeValue) return null; const dt = new Date(`${dateValue}T${timeValue}:00`); return Number.isNaN(dt.getTime()) ? null : dt; }, [dateValue, timeValue]); const [preview, setPreview] = useState<{ label: string; relative: string } | null>(null); const updatePreview = useCallback(() => { const sendAt = getSendAt(); if (!sendAt) { setPreview(null); return; } const diffMs = sendAt.getTime() - Date.now(); if (diffMs < 60_000) { setPreview(null); return; } setPreview({ label: formatSendAt(sendAt), relative: formatRelativeTime(diffMs) }); }, [getSendAt]); useEffect(() => { updatePreview(); }, [updatePreview]); const handleSubmit: FormEventHandler = async (e) => { e.preventDefault(); if (submitting) return; const sendAt = getSendAt(); if (!sendAt) { setError('Please select a valid date and 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