Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 f30a65eb74 fix(status): banner Details strips Markdown syntax, not every #, _, >, * (#308)
CI / Build & Quality Checks (pull_request) Successful in 3m20s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 17m3s
plain() deleted those characters everywhere, so "join #lotus-general"
lost its #, snake_case and @some_user lost underscores, "->" became
"-", "5 * 3" became "5  3", and "<b>" became "<b". Strip only syntax:
heading / quote / list markers at line start, paired **x** *x* _x_ `x`,
and HTML tags.

Fixes #308

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 20:22:12 -04:00
14 changed files with 59 additions and 78 deletions
+1 -1
View File
@@ -297,7 +297,7 @@ export function AccountDataEditor({
</Chip>
</Box>
<Box shrink="No">
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Box>
+5 -21
View File
@@ -111,8 +111,6 @@ type IncomingCallInfo = {
intent?: string;
notificationType: RTCNotificationType;
refEventId: string;
/** [Gitea #313] The rtc.notification event itself: what a decline must reference (MSC4310). */
notificationEventId?: string;
};
type IncomingCallProps = {
dm: boolean;
@@ -285,9 +283,7 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
fill="Soft"
size="400"
radii="400"
onClick={() =>
dm ? onReject(room, info.notificationEventId ?? info.refEventId) : onIgnore()
}
onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
before={<Icon size="200" src={Icons.Cross} filled />}
>
<Text as="span" size="B400">
@@ -459,9 +455,7 @@ function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: Incoming
size="300"
radii="300"
outlined
onClick={() =>
dm ? onReject(room, info.notificationEventId ?? info.refEventId) : onIgnore()
}
onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
before={<Icon size="100" src={Icons.Cross} filled />}
>
<Text as="span" size="B300">
@@ -602,7 +596,6 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
: undefined,
notificationType,
refEventId,
notificationEventId: event.getId(),
};
// [Gitea #312] Don't ring for a call that is already over or handled: after
@@ -611,10 +604,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
// stale RTC session). The caller's membership can arrive just after the
// ring, so an absent one gets a short grace period before giving up.
const myUserId = mx.getSafeUserId();
if (
declinedRing(room, [refEventId, event.getId()], myUserId) ||
hasActiveCallMembership(room, myUserId)
) {
if (declinedRing(room, refEventId, myUserId) || hasActiveCallMembership(room, myUserId)) {
return;
}
if (hasActiveCallMembership(room, sender)) {
@@ -623,10 +613,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
}
setTimeout(() => {
if (Date.now() >= senderTs + lifetime) return;
if (
declinedRing(room, [refEventId, event.getId()], myUserId) ||
hasActiveCallMembership(room, myUserId)
) {
if (declinedRing(room, refEventId, myUserId) || hasActiveCallMembership(room, myUserId)) {
return;
}
if (hasActiveCallMembership(room, sender)) setCallInfo(info);
@@ -698,10 +685,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
if (eventRoom?.roomId !== room.roomId || !data.liveEvent) return;
if (event.getType() === EventType.RTCDecline && event.getSender() === myUserId) {
const related = event.getRelation()?.event_id;
// [Gitea #313] Correct clients reference the notification; older Lotus
// builds referenced the call membership. Accept both.
if (!related || related === refEventId || related === callInfo.notificationEventId)
dismiss(); // declined elsewhere
if (!related || related === refEventId) dismiss(); // declined elsewhere
}
};
session.on(MatrixRTCSessionEvent.MembershipsChanged, checkMemberships);
@@ -315,7 +315,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
Setup Device Verification
</Text>
</Box>
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
@@ -361,7 +361,7 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
Reset Device Verification
</Text>
</Box>
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
@@ -275,7 +275,7 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
</Chip>
</Box>
<Box shrink="No">
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Box>
+1 -1
View File
@@ -143,7 +143,7 @@ export function PollCreator({ roomId, threadRootId, onClose }: PollCreatorProps)
Create Poll
</Text>
</Box>
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
+1 -1
View File
@@ -118,7 +118,7 @@ export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) {
Report Room
</Text>
</Box>
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
+1 -1
View File
@@ -124,7 +124,7 @@ export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
Report User
</Text>
</Box>
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
+1 -4
View File
@@ -138,7 +138,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
import { useIsDirectRoom } from '../../hooks/useRoom';
import { declinedRing, isRingableRoom } from '../../utils/callRing';
import { isRingableRoom } from '../../utils/callRing';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useRoomCreators } from '../../hooks/useRoomCreators';
@@ -2037,9 +2037,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
// Same rule as ringing: no "Missed call" in voice rooms or space
// channels, where starting a call is just joining the channel.
if (!isRingableRoom(room, direct)) return null;
// [Gitea #313] A call we declined isn't a missed call.
const ringRef = mEvent.getRelation()?.event_id;
if (declinedRing(room, [mEventId, ringRef], mx.getSafeUserId())) return null;
const content = mEvent.getContent();
const mention =
content['m.mentions']?.room ||
@@ -202,7 +202,7 @@ export function ScheduleMessageModal({
{title}
</Text>
</Box>
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
+2 -14
View File
@@ -725,13 +725,7 @@ export const MessageDeleteItem = as<
Delete Message
</Text>
</Box>
<IconButton
type="button"
size="300"
onClick={handleClose}
radii="300"
aria-label="Close"
>
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
@@ -871,13 +865,7 @@ export const MessageReportItem = as<
Report Message
</Text>
</Box>
<IconButton
type="button"
size="300"
onClick={handleClose}
radii="300"
aria-label="Close"
>
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
-15
View File
@@ -67,18 +67,3 @@ test('declinedRing finds our own decline of that ring only (#312)', () => {
assert.equal(declinedRing(room, '$ring2', '@me:x'), false, "someone else's decline");
assert.equal(declinedRing(room, '$ring3', '@me:x'), false);
});
test('declinedRing matches a decline of the notification or of the membership (#313)', () => {
const decline = (rel: string) => ({
getType: () => 'org.matrix.msc4310.rtc.decline',
getSender: () => '@me:x',
getRelation: () => ({ event_id: rel }),
});
const room = stateRoom([], [decline('$notification')]);
assert.equal(declinedRing(room, ['$membership', '$notification'], '@me:x'), true);
assert.equal(declinedRing(room, ['$membership'], '@me:x'), false);
assert.equal(
declinedRing(stateRoom([], [decline('$membership')]), ['$membership', undefined], '@me:x'),
true,
);
});
+2 -13
View File
@@ -40,18 +40,7 @@ export const hasActiveCallMembership = (room: Room, userId: string): boolean =>
* declined it (our own RTCDecline relates to it) — e.g. replayed together with
* the ring in the first sync after a restart, before any dismiss listener.
*/
export const declinedRing = (
room: Room,
ringIds: string | Array<string | undefined>,
myUserId: string,
): boolean => {
// [Gitea #313] A decline references the notification event (MSC4310); older
// Lotus builds referenced the call membership instead. Match either.
const ids = new Set((Array.isArray(ringIds) ? ringIds : [ringIds]).filter(Boolean));
return declinedAny(room, ids, myUserId);
};
const declinedAny = (room: Room, ids: Set<string | undefined>, myUserId: string): boolean =>
export const declinedRing = (room: Room, refEventId: string, myUserId: string): boolean =>
room
.getLiveTimeline()
.getEvents()
@@ -59,5 +48,5 @@ const declinedAny = (room: Room, ids: Set<string | undefined>, myUserId: string)
(ev) =>
ev.getType() === 'org.matrix.msc4310.rtc.decline' &&
ev.getSender() === myUserId &&
ids.has(ev.getRelation()?.event_id),
ev.getRelation()?.event_id === refEventId,
);
+25
View File
@@ -1,6 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
plain,
DEFAULT_GROUPS,
kumaBeatTime,
kumaUrls,
@@ -260,3 +261,27 @@ test('a dismissed calls-down banner lets the next one through, never hides serve
'server-down',
);
});
test('plain: strips Markdown syntax only, keeps ordinary #, _, > and * (#308)', () => {
assert.equal(
plain('join #lotus-general or #announcements:lotusguild.org'),
'join #lotus-general or #announcements:lotusguild.org',
);
assert.equal(
plain('ping @some_user about snake_case and C#'),
'ping @some_user about snake_case and C#',
);
assert.equal(
plain('calls -> fixed, > 5 people, 5 * 3 = 15'),
'calls -> fixed, > 5 people, 5 * 3 = 15',
);
assert.equal(
plain('# Heading\n* item one\n- item two\n> quoted'),
'Heading\nitem one\nitem two\nquoted',
);
assert.equal(plain('**bold**, *em*, _em_, `/sfu/get`'), 'bold, em, em, /sfu/get');
assert.equal(
plain('Read the <b>guide</b> <img src=x onerror=alert(1)> [docs](https://x.y)'),
'Read the guide docs',
);
});
+16 -3
View File
@@ -240,12 +240,25 @@ const incidentTone = (style: string): BannerTone => {
};
/** Markdown-ish incident text as plain text for the Details line. */
const plain = (s: string): string =>
/**
* [Gitea #308] Markdown → plain text for the Details line. Strips only the
* syntax: removing every `*_\`#>` also broke "#lotus-general", "snake_case",
* "->" and "5 * 3". HTML tags are dropped (React escapes the text anyway).
*/
export const plain = (s: string): string =>
s
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
.replace(/[*_`#>]/g, '')
.replace(/\s+\n/g, '\n')
.replace(/<\/?[a-z][^>]*>/gi, '')
.replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '')
.replace(/^[ \t]*>[ \t]?/gm, '')
.replace(/^[ \t]*[*+-][ \t]+/gm, '')
.replace(/`([^`\n]+)`/g, '$1')
.replace(/\*\*(?=\S)([^*\n]*?\S)\*\*/g, '$1')
.replace(/__(?=\S)([^_\n]*?\S)__/g, '$1')
.replace(/(^|[^\w*])\*(?=\S)([^*\n]*?\S)\*(?![\w*])/g, '$1$2')
.replace(/(^|[^\w])_(?=\S)([^_\n]*?\S)_(?!\w)/g, '$1$2')
.replace(/[ \t]+\n/g, '\n')
.trim();
/**