fix: comprehensive P0 quality pass — audit findings resolved

- ReportRoomModal: use mx.reportRoom() SDK method, fix undefined CSS vars
  (--mx-surface/border → folds color tokens), add role/aria-modal/aria-labelledby,
  accessible select/input labels, per-error-code messages, auto-close on success
- About.tsx: clickable matrix_id + email_address links (Text as="a"), AbortController
  cleanup, runtime JSON type guard, loading state, role display for all role values,
  remove classList theming hack, use mx.getHomeserverUrl()
- RoomViewHeader: useLocalRoomName for header title, useReportRoomSupported gate,
  hide Invite/Settings/Report for server notice rooms, isCreator guard on Report,
  FocusTrap returnFocusOnDeactivate on topic overlay, Server Notice chip tooltip
- RoomInput: replace raw <div> with folds <Box> for server notice read-only message
- EditHistoryModal: isRawEditEvent type guard, handle next_batch truncation,
  getVersionBody handles formatted_body (strips HTML for text display),
  role/aria-modal/aria-labelledby accessibility, guard for undefined eventId,
  use config.space tokens (remove var(--mx-spacing-*) strings)
- RoomNavItem: remove duplicate getExistingContent (use exported getLocalRoomNamesContent),
  maxLength={255} on rename input, fix FocusTrap nesting (renameDialog state moved to
  RoomNavItem_, RenameRoomDialog rendered outside menu, menu closes before dialog opens),
  pencil icon opacity via config.opacity.P300
- useRoomMeta: export getLocalRoomNamesContent for reuse
- RoomIntro: useLocalRoomName, formatted topic viewer with Overlay/FocusTrap/RoomTopicViewer
- CallRoomName: useLocalRoomName for consistent rename display in call overlay
- General.tsx: fix #980000/#FF6B00 hardcoded hex → color tokens/CSS vars, URL Preview
  capitalization, improved encrypted preview warning text + Warning chip, add
  description to plain urlPreview setting
- sanitize.ts: fix hex color regex to support 3/4/6/8 digit hex (CSS4 #RGBA, #RRGGBBAA)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 21:30:27 -04:00
parent 3db87db03f
commit 16dddcb9f0
11 changed files with 363 additions and 213 deletions
@@ -13,6 +13,7 @@ import {
Scroll,
Spinner,
Text,
config,
} from 'folds';
import { MatrixEvent, Method, Room } from 'matrix-js-sdk';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
@@ -22,17 +23,48 @@ import { timeDayMonYear, timeHourMinute } from '../../../utils/time';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
type RawEditEvent = {
type: string;
content: Record<string, unknown>;
origin_server_ts: number;
event_id: string;
};
type EditHistoryResponse = {
chunk: Array<Record<string, unknown>>;
next_batch?: string;
};
type EditHistoryData = {
events: MatrixEvent[];
hasMore: boolean;
};
type EditHistoryModalProps = {
room: Room;
mEvent: MatrixEvent;
onClose: () => void;
};
function isRawEditEvent(raw: unknown): raw is RawEditEvent {
if (typeof raw !== 'object' || raw === null) return false;
const r = raw as Record<string, unknown>;
return typeof r.event_id === 'string' && typeof r.origin_server_ts === 'number';
}
function getVersionBody(evt: MatrixEvent): string {
const content = evt.getContent();
const newContent = content['m.new_content'] as Record<string, unknown> | undefined;
const source = newContent ?? content;
const formattedBody = source.formatted_body;
if (typeof formattedBody === 'string') {
return formattedBody.replace(/<[^>]+>/g, '').trim() || '(no text)';
}
const body = source.body;
return typeof body === 'string' ? body : '(no text)';
}
export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProps) {
const mx = useMatrixClient();
const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
@@ -41,43 +73,32 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
const eventId = mEvent.getId();
const roomId = room.roomId;
const [historyState, fetchHistory] = useAsyncCallback<MatrixEvent[], unknown, []>(
const [historyState, fetchHistory] = useAsyncCallback<EditHistoryData, unknown, []>(
useCallback(async () => {
const path = `/rooms/${encodeURIComponent(roomId)}/relations/${encodeURIComponent(eventId ?? '')}/m.replace`;
if (!eventId) return { events: [], hasMore: false };
const path = `/rooms/${encodeURIComponent(roomId)}/relations/${encodeURIComponent(eventId)}/m.replace`;
const res = await mx.http.authedRequest<EditHistoryResponse>(Method.Get, path, {
limit: '50',
});
const rawEvents = res.chunk ?? [];
// Sort oldest first
const events = rawEvents
.map(
(raw) =>
// Build a lightweight representation for display; we just need content + ts
raw as {
type: string;
content: Record<string, unknown>;
origin_server_ts: number;
event_id: string;
},
)
.sort((a, b) => a.origin_server_ts - b.origin_server_ts);
// Convert to MatrixEvent-like objects using the raw data
// We use MatrixEvent if available from the timeline, otherwise parse the raw data
return events.map((raw) => {
const existing = room.findEventById(raw.event_id);
if (existing) return existing;
// Create a minimal event wrapper
const evt = new MatrixEvent({
type: raw.type,
content: raw.content,
origin_server_ts: raw.origin_server_ts,
event_id: raw.event_id,
room_id: roomId,
sender: mEvent.getSender() ?? '',
.filter(isRawEditEvent)
.sort((a, b) => a.origin_server_ts - b.origin_server_ts)
.map((raw) => {
const existing = room.findEventById(raw.event_id);
if (existing) return existing;
return new MatrixEvent({
type: raw.type,
content: raw.content,
origin_server_ts: raw.origin_server_ts,
event_id: raw.event_id,
room_id: roomId,
sender: mEvent.getSender() ?? '',
});
});
return evt;
});
return { events, hasMore: !!res.next_batch };
}, [mx, roomId, eventId, room, mEvent]),
);
@@ -91,14 +112,6 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
return `${date} at ${time}`;
};
const getVersionBody = (evt: MatrixEvent): string => {
const content = evt.getContent();
const newContent = content['m.new_content'] as Record<string, unknown> | undefined;
const body = newContent?.body ?? content.body;
return typeof body === 'string' ? body : '(no text)';
};
// The original message body (before any edits)
const originalBody = (() => {
const content = mEvent.getContent();
const body = content.body;
@@ -118,14 +131,20 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
escapeDeactivates: stopPropagation,
}}
>
<Modal variant="Surface" size="500">
<Modal
variant="Surface"
size="500"
role="dialog"
aria-modal="true"
aria-labelledby="edit-history-title"
>
<Header
variant="Surface"
size="500"
style={{ padding: '0 var(--mx-spacing-s200) 0 var(--mx-spacing-s400)' }}
style={{ padding: `0 ${config.space.S200} 0 ${config.space.S400}` }}
>
<Box grow="Yes">
<Text as="h2" size="H4" truncate>
<Text as="h2" id="edit-history-title" size="H4" truncate>
Edit History
</Text>
</Box>
@@ -139,15 +158,15 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
direction="Column"
gap="200"
style={{
padding: 'var(--mx-spacing-s400)',
paddingBottom: 'var(--mx-spacing-s700)',
padding: config.space.S400,
paddingBottom: config.space.S700,
}}
>
{historyState.status === AsyncStatus.Loading && (
<Box
justifyContent="Center"
alignItems="Center"
style={{ padding: 'var(--mx-spacing-s400)' }}
style={{ padding: config.space.S400 }}
>
<Spinner size="200" />
</Box>
@@ -159,7 +178,6 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
)}
{historyState.status === AsyncStatus.Success && (
<Box direction="Column" gap="300">
{/* Original message always shown first */}
<Box direction="Column" gap="100">
<Box gap="200" alignItems="Center">
<Text size="L400">Original</Text>
@@ -172,11 +190,11 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
</Text>
</Box>
{historyState.data.map((editEvt, index) => (
{historyState.data.events.map((editEvt, index) => (
<Box key={editEvt.getId() ?? index} direction="Column" gap="100">
<Box gap="200" alignItems="Center">
<Text size="L400">
{index === historyState.data.length - 1
{index === historyState.data.events.length - 1
? `Edit ${index + 1} (current)`
: `Edit ${index + 1}`}
</Text>
@@ -193,11 +211,19 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
</Box>
))}
{historyState.data.length === 0 && (
{historyState.data.events.length === 0 && (
<Text size="T300" priority="300">
No edit history found.
</Text>
)}
{historyState.data.hasMore && (
<Box justifyContent="Center" style={{ padding: config.space.S200 }}>
<Text size="T200" priority="300">
Showing the 50 most recent edits
</Text>
</Box>
)}
</Box>
)}
</Box>