feat: server support contact display (MSC1929) and server notices UI

- Settings Help/About fetches /.well-known/matrix/support and displays
  admin contact + support page link (graceful 404 degradation)
- Server notice rooms (m.server_notice) now show a Warning badge in the
  room header and hide the message composer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:15:01 -04:00
parent 572bc6a4c0
commit 134ebb231d
3 changed files with 114 additions and 1 deletions
+5
View File
@@ -129,6 +129,11 @@ Emoji reaction buttons styled for terminal mode via `button[data-reaction-key]`
- **Presence badges on members**: Online/busy/away dots shown next to users in the room members drawer and settings members panel (`PresenceBadge` component from `src/app/components/presence/Presence.tsx`).
- **Document title unread count**: Tab title updates to `(N) Lotus Chat` for mentions, `· Lotus Chat` for unreads, `Lotus Chat` when clear.
### Server Integration
- **Server support contact (MSC1929)**: Settings → Help & About displays the homeserver admin contact fetched from `/.well-known/matrix/support`. Shows the admin's Matrix ID and a link to the support page when the homeserver has configured this endpoint. Degrades gracefully when not configured (section is hidden on 404 or network error). In TDS mode the contact text and link render in `--lt-accent-cyan`. Implemented in `src/app/features/settings/about/About.tsx`.
- **Server notices**: Rooms of type `m.server_notice` (system messages from the homeserver) now render with a distinct "Server Notice" `<Chip variant="Warning">` badge in the room header and a disabled composer showing "This is a server notice room — you cannot send messages here." Previously indistinguishable from regular DMs. Badge in `src/app/features/room/RoomViewHeader.tsx`; composer guard in `src/app/features/room/RoomInput.tsx`.
### Infrastructure
- **Authenticated media**: All avatar/media loads use `mxcUrlToHttp(mx, mxcUrl, useAuthentication, w, h, 'crop')` from `../../utils/matrix` — the Lotus utility that handles MSC3916 authenticated media. (Upstream Cinny uses the SDK method with incorrect argument order for authenticated endpoints.)
+10
View File
@@ -603,6 +603,16 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
});
};
if (room.getType() === 'm.server_notice') {
return (
<div ref={ref} style={{ padding: config.space.S300, textAlign: 'center' }}>
<Text size="T300" priority="300">
This is a server notice room you cannot send messages here.
</Text>
</div>
);
}
return (
<div ref={ref}>
{selectedFiles.length > 0 && (
+99 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll, Button, config, toRem } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
@@ -8,12 +8,49 @@ 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;
email_address?: string;
role?: string;
};
type MSC1929Support = {
contacts?: MSC1929Contact[];
support_page?: string;
};
function useServerSupport(): MSC1929Support | null {
const mx = useMatrixClient();
const [support, setSupport] = useState<MSC1929Support | null>(null);
useEffect(() => {
const baseUrl = (mx as unknown as { baseUrl: string }).baseUrl;
fetch(`${baseUrl}/.well-known/matrix/support`)
.then((res) => {
if (!res.ok) return null;
return res.json() as Promise<MSC1929Support>;
})
.then((data) => {
if (data && (data.contacts?.length || data.support_page)) {
setSupport(data);
}
})
.catch(() => {
// Graceful degradation — server may not have this configured
});
}, [mx]);
return support;
}
type AboutProps = {
requestClose: () => void;
};
export function About({ requestClose }: AboutProps) {
const mx = useMatrixClient();
const serverSupport = useServerSupport();
return (
<Page>
@@ -108,6 +145,67 @@ export function About({ requestClose }: AboutProps) {
/>
</SequenceCard>
</Box>
{serverSupport && (
<Box direction="Column" gap="100">
<Text size="L400">Homeserver Support</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<Box direction="Column" gap="200" style={{ padding: config.space.S200 }}>
{serverSupport.contacts && serverSupport.contacts.length > 0 && (
<Box direction="Column" gap="100">
{serverSupport.contacts.map((contact, i) => (
<Box key={i} alignItems="Center" gap="200">
<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 ?? ''}
</Text>
</Box>
))}
</Box>
)}
{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>
</Box>
)}
</Box>
</SequenceCard>
</Box>
)}
<Box direction="Column" gap="100">
<Text size="L400">Credits</Text>
<SequenceCard