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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user