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
+75 -45
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll, Button, config, toRem } from 'folds';
import { Box, Text, IconButton, Icon, Icons, Scroll, Button, Spinner, config, toRem } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
@@ -8,7 +8,6 @@ import LotusLogo from '../../../../../public/res/Lotus.png';
import pkg from '../../../../../package.json';
import { clearCacheAndReload } from '../../../../client/initMatrix';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { lotusTerminalBodyClass } from '../../../../lotus-terminal.css';
type MSC1929Contact = {
matrix_id?: string;
@@ -21,28 +20,50 @@ type MSC1929Support = {
support_page?: string;
};
function useServerSupport(): MSC1929Support | null {
function isMSC1929Support(data: unknown): data is MSC1929Support {
if (typeof data !== 'object' || data === null) return false;
const d = data as Record<string, unknown>;
if ('contacts' in d && !Array.isArray(d.contacts)) return false;
if ('support_page' in d && typeof d.support_page !== 'string') return false;
return true;
}
function formatRole(role?: string): string {
if (!role) return 'Contact';
if (role === 'm.role.admin') return 'Admin';
if (role === 'm.role.security') return 'Security';
return role.startsWith('m.role.') ? role.slice(7) : role;
}
function useServerSupport(): { support: MSC1929Support | null; loading: boolean } {
const mx = useMatrixClient();
const [support, setSupport] = useState<MSC1929Support | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const baseUrl = (mx as unknown as { baseUrl: string }).baseUrl;
fetch(`${baseUrl}/.well-known/matrix/support`)
const controller = new AbortController();
const baseUrl = mx.getHomeserverUrl();
setLoading(true);
fetch(`${baseUrl}/.well-known/matrix/support`, { signal: controller.signal })
.then((res) => {
if (!res.ok) return null;
return res.json() as Promise<MSC1929Support>;
return res.json();
})
.then((data) => {
if (data && (data.contacts?.length || data.support_page)) {
if (isMSC1929Support(data) && (data.contacts?.length || data.support_page)) {
setSupport(data);
}
})
.catch(() => {
// Graceful degradation — server may not have this configured
});
.catch((e) => {
if (e.name !== 'AbortError') {
// Graceful degradation — server may not have this configured
}
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [mx]);
return support;
return { support, loading };
}
type AboutProps = {
@@ -50,7 +71,7 @@ type AboutProps = {
};
export function About({ requestClose }: AboutProps) {
const mx = useMatrixClient();
const serverSupport = useServerSupport();
const { support: serverSupport, loading: supportLoading } = useServerSupport();
return (
<Page>
@@ -145,7 +166,7 @@ export function About({ requestClose }: AboutProps) {
/>
</SequenceCard>
</Box>
{serverSupport && (
{(serverSupport || supportLoading) && (
<Box direction="Column" gap="100">
<Text size="L400">Homeserver Support</Text>
<SequenceCard
@@ -155,50 +176,59 @@ export function About({ requestClose }: AboutProps) {
gap="400"
>
<Box direction="Column" gap="200" style={{ padding: config.space.S200 }}>
{serverSupport.contacts && serverSupport.contacts.length > 0 && (
{supportLoading && !serverSupport && (
<Box alignItems="Center" gap="200">
<Spinner size="100" variant="Secondary" />
<Text size="T300" priority="300">Loading support info</Text>
</Box>
)}
{serverSupport?.contacts && serverSupport.contacts.length > 0 && (
<Box direction="Column" gap="100">
{serverSupport.contacts.map((contact, i) => (
<Box key={i} alignItems="Center" gap="200">
<Box key={i} direction="Column" gap="100">
<Text size="T300" priority="300">
{contact.role === 'm.role.admin'
? 'Admin'
: contact.role === 'm.role.security'
? 'Security'
: 'Contact'}
:
</Text>
<Text
size="T300"
style={{
color: document.body.classList.contains(lotusTerminalBodyClass)
? 'var(--lt-accent-cyan)'
: undefined,
}}
>
{contact.matrix_id ?? contact.email_address ?? ''}
{formatRole(contact.role)}:
</Text>
<Box direction="Column" gap="100" style={{ paddingLeft: config.space.S200 }}>
{contact.matrix_id && (
<Text
as="a"
href={`https://matrix.to/#/${encodeURIComponent(contact.matrix_id)}`}
target="_blank"
rel="noreferrer noopener"
size="T300"
>
{contact.matrix_id}
</Text>
)}
{contact.email_address && (
<Text
as="a"
href={`mailto:${contact.email_address}`}
size="T300"
>
{contact.email_address}
</Text>
)}
</Box>
</Box>
))}
</Box>
)}
{serverSupport.support_page && (
{serverSupport?.support_page && (
<Box alignItems="Center" gap="200">
<Text size="T300" priority="300">
Support Page:
</Text>
<Text size="T300">
<a
href={serverSupport.support_page}
target="_blank"
rel="noreferrer noopener"
style={{
color: document.body.classList.contains(lotusTerminalBodyClass)
? 'var(--lt-accent-cyan)'
: undefined,
}}
>
{serverSupport.support_page}
</a>
<Text
as="a"
href={serverSupport.support_page}
target="_blank"
rel="noreferrer noopener"
aria-label="Open support page"
size="T300"
>
{serverSupport.support_page}
</Text>
</Box>
)}
+26 -7
View File
@@ -14,6 +14,7 @@ import {
Box,
Button,
Chip,
color,
config,
Header,
Icon,
@@ -408,8 +409,8 @@ function Appearance() {
title="Replay boot sequence"
style={{
background: 'transparent',
border: '1px solid rgba(255,107,0,0.35)',
color: '#FF6B00',
border: '1px solid var(--accent-orange-border)',
color: 'var(--accent-orange)',
fontSize: '0.65rem',
padding: '0.2rem 0.6rem',
cursor: 'pointer',
@@ -964,14 +965,14 @@ function ChatBgGrid() {
cursor: 'pointer',
border:
chatBackground === opt.value
? '2px solid #980000'
? `2px solid ${color.Critical.Main}`
: '2px solid rgba(128,128,128,0.25)',
padding: 0,
overflow: 'hidden',
...getChatBg(opt.value as ChatBackground, isDark),
}}
/>
<Text size="T200" style={chatBackground === opt.value ? { color: '#980000' } : undefined}>
<Text size="T200" style={chatBackground === opt.value ? { color: color.Critical.Main } : undefined}>
{opt.label}
</Text>
</Box>
@@ -1195,14 +1196,32 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Url Preview"
title="URL Preview"
description="Your homeserver fetches and caches link previews. It will see the URLs of all links you preview."
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Url Preview in Encrypted Room"
description="URL previews in encrypted rooms are fetched by your homeserver, which sees the URL but not the message content."
title="URL Preview in Encrypted Rooms"
description={
<Box direction="Column" gap="100">
<Text size="T200" priority="300">
Your homeserver fetches link previews on your behalf. It cannot decrypt your
messages, but will see every URL you preview in encrypted rooms, potentially
revealing conversation topics.
</Text>
<Chip
variant="Warning"
fill="Soft"
radii="300"
size="400"
style={{ alignSelf: 'flex-start' }}
>
<Text size="T200">Privacy risk enabled by default</Text>
</Chip>
</Box>
}
after={<Switch variant="Primary" value={encUrlPreview} onChange={setEncUrlPreview} />}
/>
</SequenceCard>