diff --git a/.changeset/add_graceful_fail_if_msc4140_event_delay_exceeded.md b/.changeset/add_graceful_fail_if_msc4140_event_delay_exceeded.md new file mode 100644 index 000000000..35397e298 --- /dev/null +++ b/.changeset/add_graceful_fail_if_msc4140_event_delay_exceeded.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +# Add graceful fail if MSC4140 event delay exceeded diff --git a/src/app/cs-errorcode.ts b/src/app/cs-errorcode.ts index 6c21d670c..4e54d5ac0 100644 --- a/src/app/cs-errorcode.ts +++ b/src/app/cs-errorcode.ts @@ -34,4 +34,5 @@ export enum ErrorCode { M_EXCLUSIVE = 'M_EXCLUSIVE', M_RESOURCE_LIMIT_EXCEEDED = 'M_RESOURCE_LIMIT_EXCEEDED', M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM', + M_MAX_DELAY_EXCEEDED = 'M_MAX_DELAY_EXCEEDED', } diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index b280cf5d5..0f2d7811c 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -9,11 +9,12 @@ import { useRef, useState, } from 'react'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { isKeyHotkey } from 'is-hotkey'; import { EventType, IContent, + MatrixError, MsgType, RelationType, Room, @@ -24,6 +25,7 @@ import { ReactEditor } from 'slate-react'; import { Editor, Point, Range, Transforms } from 'slate'; import { Box, + color, config, Dialog, Icon, @@ -135,6 +137,7 @@ import { delayedEventsSupportedAtom, roomIdToScheduledTimeAtomFamily, roomIdToEditingScheduledDelayIdAtomFamily, + serverMaxDelayMsAtom, } from '$state/scheduledMessages'; import { sendDelayedMessage, @@ -149,6 +152,7 @@ import { usePowerLevelsContext } from '$hooks/usePowerLevels'; import { useRoomCreators } from '$hooks/useRoomCreators'; import { useRoomPermissions } from '$hooks/useRoomPermissions'; import { AutocompleteNotice } from '$components/editor/autocomplete/AutocompleteNotice'; +import { ErrorCode } from '../../cs-errorcode'; import { SchedulePickerDialog } from './schedule-send'; import * as css from './schedule-send/SchedulePickerDialog.css'; import { @@ -313,6 +317,8 @@ export const RoomInput = forwardRef( const [showSchedulePicker, setShowSchedulePicker] = useState(false); const [silentReply, setSilentReply] = useState(false); const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); + const setServerMaxDelayMs = useSetAtom(serverMaxDelayMsAtom); + const [sendError, setSendError] = useState(); const isEncrypted = room.hasEncryptionStateEvent(); useElementSizeObserver( @@ -638,12 +644,21 @@ export const RoomInput = forwardRef( } else { await sendDelayedMessage(mx, roomId, content, delayMs); } + setSendError(undefined); invalidate(); setEditingScheduledDelayId(null); setScheduledTime(null); resetInput(); - } catch { - // Network/server error — leave editor and scheduled state intact for retry + } catch (e: unknown) { + if (e instanceof MatrixError && e.errcode === ErrorCode.M_MAX_DELAY_EXCEEDED) { + const maxDelay = (e.data as { max_delay?: number })?.max_delay; + if (typeof maxDelay === 'number') setServerMaxDelayMs(maxDelay); + setSendError( + 'Scheduled time exceeds the maximum delay allowed by this server. Please choose an earlier time.' + ); + } else { + setSendError('Failed to schedule message. Please try again.'); + } } } else if (editingScheduledDelayId) { try { @@ -702,6 +717,8 @@ export const RoomInput = forwardRef( setEditingScheduledDelayId, setScheduledTime, room, + setServerMaxDelayMs, + setSendError, ]); const handleKeyDown: KeyboardEventHandler = useCallback( @@ -943,6 +960,7 @@ export const RoomInput = forwardRef( onClick={() => { setScheduledTime(null); setEditingScheduledDelayId(null); + setSendError(undefined); }} variant="SurfaceVariant" size="300" @@ -961,6 +979,19 @@ export const RoomInput = forwardRef( )} + {sendError && ( +
+ + + {sendError} + + +
+ )} {replyDraft && (!threadRootId || replyDraft.body) && (
( onSubmit={(date) => { setScheduledTime(date); setShowSchedulePicker(false); + setSendError(undefined); }} /> )} diff --git a/src/app/features/room/schedule-send/SchedulePickerDialog.tsx b/src/app/features/room/schedule-send/SchedulePickerDialog.tsx index 50f119ef9..bb5154c72 100644 --- a/src/app/features/room/schedule-send/SchedulePickerDialog.tsx +++ b/src/app/features/room/schedule-send/SchedulePickerDialog.tsx @@ -1,4 +1,6 @@ import { MouseEventHandler, useState } from 'react'; +import { useAtomValue } from 'jotai'; +import { serverMaxDelayMsAtom } from '$state/scheduledMessages'; import FocusTrap from 'focus-trap-react'; import { Dialog, @@ -38,7 +40,9 @@ export function SchedulePickerDialog({ onSubmit, }: SchedulePickerDialogProps) { const now = Date.now(); - const maxDelay = daysToMs(30); + const serverMaxDelayMs = useAtomValue(serverMaxDelayMsAtom); + const maxDelay = serverMaxDelayMs ?? daysToMs(30); + const maxDays = Math.round(maxDelay / daysToMs(1)); const defaultTs = initialTime ?? now + hoursToMs(1); const [ts, setTs] = useState(() => Math.max(defaultTs, now + 60000)); const [error, setError] = useState(); @@ -66,7 +70,7 @@ export function SchedulePickerDialog({ return; } if (delay > maxDelay) { - setError('Cannot schedule more than 30 days in advance'); + setError(`Cannot schedule more than ${maxDays} day${maxDays !== 1 ? 's' : ''} in advance`); return; } setError(undefined); diff --git a/src/app/state/scheduledMessages.ts b/src/app/state/scheduledMessages.ts index ba28b60eb..2916a78b0 100644 --- a/src/app/state/scheduledMessages.ts +++ b/src/app/state/scheduledMessages.ts @@ -14,3 +14,5 @@ export const roomIdToEditingScheduledDelayIdAtomFamily = atomFamily< string, ReturnType> >(() => atom(null)); + +export const serverMaxDelayMsAtom = atom(null);