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..f7c79c885 --- /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 850cffb50..c6b2ab2f4 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -1,6 +1,7 @@ import type { KeyboardEventHandler, MouseEvent, RefObject } from 'react'; import { forwardRef, useCallback, useEffect, useRef, useState, useMemo } from 'react'; -import { useAtom, useAtomValue } from 'jotai'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; + import { isKeyHotkey } from 'is-hotkey'; import type { IContent, @@ -10,6 +11,7 @@ import type { RoomMessageEventContent, StickerEventContent, } from '$types/matrix-sdk'; +import { MatrixError } from '$types/matrix-sdk'; import { EventType, MsgType, RelationType } from '$types/matrix-sdk'; import { ReactEditor } from 'slate-react'; import { Editor, Point, Range, Transforms } from 'slate'; @@ -108,6 +110,7 @@ import { delayedEventsSupportedAtom, roomIdToScheduledTimeAtomFamily, roomIdToEditingScheduledDelayIdAtomFamily, + serverMaxDelayMsAtom, } from '$state/scheduledMessages'; import { sendDelayedMessage, @@ -115,7 +118,7 @@ import { computeDelayMs, cancelDelayedEvent, } from '$utils/delayedEvents'; -import { timeHourMinute, timeDayMonthYear } from '$utils/time'; +import { timeHourMinute, timeDayMonthYear, daysToMs } from '$utils/time'; import { stopPropagation } from '$utils/keyboard'; import { usePowerLevelsContext } from '$hooks/usePowerLevels'; @@ -128,6 +131,7 @@ import { } from '$hooks/usePerMessageProfile'; import { Microphone, Stop } from '@phosphor-icons/react'; import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supportedCodec'; +import { ErrorCode } from '../../cs-errorcode'; import { sanitizeText } from '$utils/sanitize'; import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler'; import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler'; @@ -381,6 +385,8 @@ export const RoomInput = forwardRef( const [showSchedulePicker, setShowSchedulePicker] = useState(false); const [silentReply, setSilentReply] = useState(!mentionInReplies); const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); + const setServerMaxDelayMs = useSetAtom(serverMaxDelayMsAtom); + const [sendError, setSendError] = useState(); const isEncrypted = room.hasEncryptionStateEvent(); useElementSizeObserver( @@ -959,12 +965,28 @@ export const RoomInput = forwardRef( } else { await sendDelayedMessage(mx, roomId, content as RoomMessageEventContent, 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 || + e.data?.['org.matrix.msc4140.errcode'] === 'M_MAX_DELAY_EXCEEDED') + ) { + const maxDelay = + (e.data as { max_delay?: number })?.max_delay ?? + e.data?.['org.matrix.msc4140.max_delay']; + if (typeof maxDelay === 'number') setServerMaxDelayMs(maxDelay); + const maxDelayDays = maxDelay / daysToMs(1); + setSendError( + `Scheduled time exceeds the maximum delay allowed by this server. Please choose an earlier time. The Maximum Delay is of ${maxDelayDays} day${maxDelayDays > 1 ? 's' : ''}.` + ); + } else { + setSendError('Failed to schedule message. Please try again.'); + } } } else if (editingScheduledDelayId) { try { @@ -1055,6 +1077,7 @@ export const RoomInput = forwardRef( isEncrypted, setEditingScheduledDelayId, setScheduledTime, + setServerMaxDelayMs, ]); const handleKeyDown: KeyboardEventHandler = useCallback( @@ -1385,6 +1408,7 @@ export const RoomInput = forwardRef( onClick={() => { setScheduledTime(null); setEditingScheduledDelayId(null); + setSendError(undefined); }} variant="SurfaceVariant" size="300" @@ -1403,6 +1427,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 bf0e2689a..f699e2567 100644 --- a/src/app/features/room/schedule-send/SchedulePickerDialog.tsx +++ b/src/app/features/room/schedule-send/SchedulePickerDialog.tsx @@ -1,5 +1,7 @@ import type { MouseEventHandler } from 'react'; import { useState } from 'react'; +import { useAtomValue } from 'jotai'; +import { serverMaxDelayMsAtom } from '$state/scheduledMessages'; import FocusTrap from 'focus-trap-react'; import type { RectCords } from 'folds'; import { @@ -39,7 +41,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(); @@ -67,7 +71,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 7bed18572..d9144b2ad 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);