Skip to content

Commit b161a41

Browse files
committed
Merge branch 'dev' into feat/presence
2 parents 2de2315 + 9882f7a commit b161a41

7 files changed

Lines changed: 104 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
Add graceful fail if MSC4140 event delay exceeded
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
Matrix.to links sent without explicit markdown formatting are sent as raw links instead of html links.

src/app/cs-errorcode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ export enum ErrorCode {
3434
M_EXCLUSIVE = 'M_EXCLUSIVE',
3535
M_RESOURCE_LIMIT_EXCEEDED = 'M_RESOURCE_LIMIT_EXCEEDED',
3636
M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM',
37+
M_MAX_DELAY_EXCEEDED = 'M_MAX_DELAY_EXCEEDED',
3738
}

src/app/features/room/RoomInput.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { KeyboardEventHandler, MouseEvent, RefObject } from 'react';
22
import { forwardRef, useCallback, useEffect, useRef, useState, useMemo } from 'react';
3-
import { useAtom, useAtomValue } from 'jotai';
3+
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
4+
45
import { isKeyHotkey } from 'is-hotkey';
56
import type {
67
IContent,
@@ -10,6 +11,7 @@ import type {
1011
RoomMessageEventContent,
1112
StickerEventContent,
1213
} from '$types/matrix-sdk';
14+
import { MatrixError } from '$types/matrix-sdk';
1315
import { EventType, MsgType, RelationType } from '$types/matrix-sdk';
1416
import { ReactEditor } from 'slate-react';
1517
import { Editor, Point, Range, Transforms } from 'slate';
@@ -108,14 +110,15 @@ import {
108110
delayedEventsSupportedAtom,
109111
roomIdToScheduledTimeAtomFamily,
110112
roomIdToEditingScheduledDelayIdAtomFamily,
113+
serverMaxDelayMsAtom,
111114
} from '$state/scheduledMessages';
112115
import {
113116
sendDelayedMessage,
114117
sendDelayedMessageE2EE,
115118
computeDelayMs,
116119
cancelDelayedEvent,
117120
} from '$utils/delayedEvents';
118-
import { timeHourMinute, timeDayMonthYear } from '$utils/time';
121+
import { timeHourMinute, timeDayMonthYear, daysToMs } from '$utils/time';
119122
import { stopPropagation } from '$utils/keyboard';
120123

121124
import { usePowerLevelsContext } from '$hooks/usePowerLevels';
@@ -128,6 +131,7 @@ import {
128131
} from '$hooks/usePerMessageProfile';
129132
import { Microphone, Stop } from '@phosphor-icons/react';
130133
import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supportedCodec';
134+
import { ErrorCode } from '../../cs-errorcode';
131135
import { sanitizeText } from '$utils/sanitize';
132136
import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler';
133137
import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler';
@@ -383,6 +387,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
383387
const [pollCreatorOpen, setPollCreatorOpen] = useState(false);
384388
const [silentReply, setSilentReply] = useState(!mentionInReplies);
385389
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
390+
const setServerMaxDelayMs = useSetAtom(serverMaxDelayMsAtom);
391+
const [sendError, setSendError] = useState<string | undefined>();
386392
const isEncrypted = room.hasEncryptionStateEvent();
387393

388394
useElementSizeObserver(
@@ -961,12 +967,28 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
961967
} else {
962968
await sendDelayedMessage(mx, roomId, content as RoomMessageEventContent, delayMs);
963969
}
970+
setSendError(undefined);
964971
invalidate();
965972
setEditingScheduledDelayId(null);
966973
setScheduledTime(null);
967974
resetInput();
968-
} catch {
969-
// Network/server error — leave editor and scheduled state intact for retry
975+
} catch (e: unknown) {
976+
if (
977+
e instanceof MatrixError &&
978+
(e.errcode === ErrorCode.M_MAX_DELAY_EXCEEDED ||
979+
e.data?.['org.matrix.msc4140.errcode'] === 'M_MAX_DELAY_EXCEEDED')
980+
) {
981+
const maxDelay =
982+
(e.data as { max_delay?: number })?.max_delay ??
983+
e.data?.['org.matrix.msc4140.max_delay'];
984+
if (typeof maxDelay === 'number') setServerMaxDelayMs(maxDelay);
985+
const maxDelayDays = maxDelay / daysToMs(1);
986+
setSendError(
987+
`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' : ''}.`
988+
);
989+
} else {
990+
setSendError('Failed to schedule message. Please try again.');
991+
}
970992
}
971993
} else if (editingScheduledDelayId) {
972994
try {
@@ -1057,6 +1079,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
10571079
isEncrypted,
10581080
setEditingScheduledDelayId,
10591081
setScheduledTime,
1082+
setServerMaxDelayMs,
10601083
]);
10611084

10621085
const handleKeyDown: KeyboardEventHandler = useCallback(
@@ -1387,6 +1410,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
13871410
onClick={() => {
13881411
setScheduledTime(null);
13891412
setEditingScheduledDelayId(null);
1413+
setSendError(undefined);
13901414
}}
13911415
variant="SurfaceVariant"
13921416
size="300"
@@ -1405,6 +1429,19 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
14051429
</Box>
14061430
</div>
14071431
)}
1432+
{sendError && (
1433+
<div>
1434+
<Box
1435+
alignItems="Center"
1436+
gap="300"
1437+
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
1438+
>
1439+
<Text style={{ color: color.Critical.Main }} size="T300">
1440+
{sendError}
1441+
</Text>
1442+
</Box>
1443+
</div>
1444+
)}
14081445
{replyDraft && (!threadRootId || replyDraft.body) && (
14091446
<div>
14101447
<Box
@@ -1738,6 +1775,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
17381775
onSubmit={(date) => {
17391776
setScheduledTime(date);
17401777
setShowSchedulePicker(false);
1778+
setSendError(undefined);
17411779
}}
17421780
/>
17431781
)}

src/app/features/room/schedule-send/SchedulePickerDialog.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { MouseEventHandler } from 'react';
22
import { useState } from 'react';
3+
import { useAtomValue } from 'jotai';
4+
import { serverMaxDelayMsAtom } from '$state/scheduledMessages';
35
import FocusTrap from 'focus-trap-react';
46
import type { RectCords } from 'folds';
57
import {
@@ -39,7 +41,9 @@ export function SchedulePickerDialog({
3941
onSubmit,
4042
}: SchedulePickerDialogProps) {
4143
const now = Date.now();
42-
const maxDelay = daysToMs(30);
44+
const serverMaxDelayMs = useAtomValue(serverMaxDelayMsAtom);
45+
const maxDelay = serverMaxDelayMs ?? daysToMs(30);
46+
const maxDays = Math.round(maxDelay / daysToMs(1));
4347
const defaultTs = initialTime ?? now + hoursToMs(1);
4448
const [ts, setTs] = useState(() => Math.max(defaultTs, now + 60000));
4549
const [error, setError] = useState<string>();
@@ -67,7 +71,7 @@ export function SchedulePickerDialog({
6771
return;
6872
}
6973
if (delay > maxDelay) {
70-
setError('Cannot schedule more than 30 days in advance');
74+
setError(`Cannot schedule more than ${maxDays} day${maxDays !== 1 ? 's' : ''} in advance`);
7175
return;
7276
}
7377
setError(undefined);

src/app/plugins/markdown/markdownToHtml.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,39 @@ const decodeHtmlEntities = (text: string): string => {
5050
return result;
5151
};
5252

53+
const MATRIX_TO_PLACEHOLDER_PREFIX = 'MATRIXTORAWLINKTOKEN';
54+
55+
const escapeHtml = (text: string): string =>
56+
text
57+
.replace(/&/g, '&amp;')
58+
.replace(/</g, '&lt;')
59+
.replace(/>/g, '&gt;')
60+
.replace(/"/g, '&quot;')
61+
.replace(/'/g, '&#39;');
62+
63+
const shieldBareMatrixToLinks = (
64+
input: string
65+
): { shielded: string; placeholders: Map<string, string> } => {
66+
const placeholders = new Map<string, string>();
67+
let index = 0;
68+
69+
const shielded = input.replace(/(?<!\]\()https?:\/\/matrix\.to\/[^\s<)]+/gi, (url) => {
70+
const key = `${MATRIX_TO_PLACEHOLDER_PREFIX}${index++}X`;
71+
placeholders.set(key, url);
72+
return key;
73+
});
74+
75+
return { shielded, placeholders };
76+
};
77+
78+
const unshieldBareMatrixToLinks = (html: string, placeholders: Map<string, string>): string => {
79+
let result = html;
80+
for (const [key, url] of placeholders.entries()) {
81+
result = result.split(key).join(escapeHtml(url));
82+
}
83+
return result;
84+
};
85+
5386
/**
5487
* Converts markdown string to sanitized Matrix-compatible HTML.
5588
* Uses marked for parsing and DOMPurify for sanitization per Matrix spec.
@@ -67,7 +100,10 @@ export function markdownToHtml(markdown: string): string {
67100

68101
const preprocessed = preprocessEmoticon(blockquotePrefixed);
69102

70-
const mathInput = shieldDollarRunsForMarked(maskDollarSignsInsideMarkdownCode(preprocessed));
103+
const { shielded: matrixToShielded, placeholders: matrixToPlaceholders } =
104+
shieldBareMatrixToLinks(preprocessed);
105+
106+
const mathInput = shieldDollarRunsForMarked(maskDollarSignsInsideMarkdownCode(matrixToShielded));
71107

72108
// Parse markdown to HTML using marked with our Matrix extensions
73109
const html = processor.parse(mathInput) as string;
@@ -164,5 +200,10 @@ export function markdownToHtml(markdown: string): string {
164200
}
165201
);
166202

167-
return restoredMxEmoticonHeight.replace(/<li>(<p><\/p>)?<\/li>/gi, '<li><br></li>');
203+
const unshieldedMatrixTo = unshieldBareMatrixToLinks(
204+
restoredMxEmoticonHeight,
205+
matrixToPlaceholders
206+
);
207+
208+
return unshieldedMatrixTo.replace(/<li>(<p><\/p>)?<\/li>/gi, '<li><br></li>');
168209
}

src/app/state/scheduledMessages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export const roomIdToEditingScheduledDelayIdAtomFamily = atomFamily<
1414
string,
1515
ReturnType<typeof atom<string | null>>
1616
>(() => atom<string | null>(null));
17+
18+
export const serverMaxDelayMsAtom = atom<number | null>(null);

0 commit comments

Comments
 (0)