import React, { FormEventHandler, useCallback, useEffect, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Box, Button, Dialog, 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'; import { useModalStyle } from '../../hooks/useModalStyle'; import { toLocalDate, toLocalTime, parseLocalDateTime, pickerInputStyle, formatFriendlyDateTime, } from '../../utils/datetimeInput'; interface ScheduleMessageModalProps { roomId: string; /** Pre-fill the message body from the composer. Pass null/undefined to open blank. */ initialBody?: string; /** Pre-fill the date/time pickers (Unix ms) — used when editing/rescheduling. */ initialSendAt?: number; /** Header title; defaults to "Schedule Message". */ title?: string; /** Primary-button label; defaults to "Schedule" (e.g. "Reschedule" when editing). */ submitLabel?: 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'; } export function ScheduleMessageModal({ roomId, initialBody, initialSendAt, title = 'Schedule Message', submitLabel = 'Schedule', onScheduled, onClose, }: ScheduleMessageModalProps) { const modalStyle = useModalStyle(400); 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; }; // When editing, seed the pickers from the existing send-time; else default to +1h. const def = initialSendAt ? new Date(initialSendAt) : defaultDate(); const [dateValue, setDateValue] = useState(() => toLocalDate(def)); const [timeValue, setTimeValue] = useState(() => toLocalTime(def)); const getSendAt = useCallback( (): Date | null => parseLocalDateTime(dateValue, timeValue), [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: formatFriendlyDateTime(sendAt.getTime()), 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 */}
{title}
{/* Body */} {/* Message input */} Message