CI / Build & Quality Checks (push) Successful in 1m39s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 7s
CI / Trigger Desktop Build (push) Successful in 7s
CI / Playwright smoke (e2e) (push) Successful in 8m41s
public/res/ carried two DIFFERENT files whose names differ only in case — Lotus.png (19 897 B, the 256px logo used by the welcome page, auth layout, OIDC config and About) and lotus.png (2 073 B, the notification icon). Windows and macOS filesystems are case-insensitive, so a checkout there collapses them into one path: one of the two references then resolves to the wrong bytes or to nothing, which is why the desktop client's home logo rendered as alt text from http://localhost:44548/public/res/Lotus.png. Renamed the logo to lotus-logo.png and updated its four references; verified no case-only filename collisions remain anywhere in the repo, and that both logos still load (256x256 on the welcome page and About). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
435 lines
17 KiB
TypeScript
435 lines
17 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
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';
|
|
import { SettingTile } from '../../../components/setting-tile';
|
|
import { getOriginBaseUrl, withOriginBaseUrl } from '../../../pages/pathUtils';
|
|
import pkg from '../../../../../package.json';
|
|
import { clearCacheAndReload } from '../../../../client/initMatrix';
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|
|
|
const LotusLogo = withOriginBaseUrl(getOriginBaseUrl(), '/public/res/lotus-logo.png');
|
|
|
|
type MSC1929Contact = {
|
|
matrix_id?: string;
|
|
email_address?: string;
|
|
role?: string;
|
|
};
|
|
|
|
type MSC1929Support = {
|
|
contacts?: MSC1929Contact[];
|
|
support_page?: string;
|
|
};
|
|
|
|
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 controller = new AbortController();
|
|
// MSC1929 support info is served from the MXID server-name host (like
|
|
// /.well-known/matrix/client), which on delegated/split-domain servers is
|
|
// NOT the client-API URL. Derive it from the user's domain.
|
|
const serverName = mx.getDomain();
|
|
if (!serverName) {
|
|
setLoading(false);
|
|
return undefined;
|
|
}
|
|
setLoading(true);
|
|
fetch(`https://${serverName}/.well-known/matrix/support`, { signal: controller.signal })
|
|
.then((res) => {
|
|
if (!res.ok) return null;
|
|
return res.json();
|
|
})
|
|
.then((data) => {
|
|
if (isMSC1929Support(data) && (data.contacts?.length || data.support_page)) {
|
|
setSupport(data);
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
if (e.name !== 'AbortError') {
|
|
// Graceful degradation — server may not have this configured
|
|
}
|
|
})
|
|
.finally(() => setLoading(false));
|
|
return () => controller.abort();
|
|
}, [mx]);
|
|
|
|
return { support, loading };
|
|
}
|
|
|
|
type AboutProps = {
|
|
requestClose: () => void;
|
|
};
|
|
export function About({ requestClose }: AboutProps) {
|
|
const mx = useMatrixClient();
|
|
const { support: serverSupport, loading: supportLoading } = useServerSupport();
|
|
|
|
return (
|
|
<Page>
|
|
<PageHeader outlined={false}>
|
|
<Box grow="Yes" gap="200">
|
|
<Box grow="Yes" alignItems="Center" gap="200">
|
|
<Text size="H3" truncate>
|
|
About
|
|
</Text>
|
|
</Box>
|
|
<Box shrink="No">
|
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
|
<Icon src={Icons.Cross} />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
</PageHeader>
|
|
<Box grow="Yes">
|
|
<Scroll hideTrack visibility="Hover">
|
|
<PageContent>
|
|
<Box direction="Column" gap="700">
|
|
<Box gap="400">
|
|
<Box shrink="No">
|
|
<img
|
|
style={{ width: toRem(60), height: toRem(60) }}
|
|
src={LotusLogo}
|
|
alt="Lotus Chat logo"
|
|
/>
|
|
</Box>
|
|
<Box direction="Column" gap="300">
|
|
<Box direction="Column" gap="100">
|
|
<Box gap="100" alignItems="End">
|
|
<Text size="H3">Lotus Chat</Text>
|
|
<Text size="T200">v{pkg.version}</Text>
|
|
</Box>
|
|
<Text>A Matrix client for Lotus Guild.</Text>
|
|
</Box>
|
|
|
|
<Box gap="200" wrap="Wrap">
|
|
<Button
|
|
as="a"
|
|
href="https://code.lotusguild.org/LotusGuild/cinny"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
variant="Secondary"
|
|
fill="Soft"
|
|
size="300"
|
|
radii="300"
|
|
before={<Icon src={Icons.Code} size="100" filled />}
|
|
>
|
|
<Text size="B300">Source Code</Text>
|
|
</Button>
|
|
<Button
|
|
as="a"
|
|
href="https://matrix.lotusguild.org"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
variant="Critical"
|
|
fill="Soft"
|
|
size="300"
|
|
radii="300"
|
|
before={<Icon src={Icons.Heart} size="100" filled />}
|
|
>
|
|
<Text size="B300">Support</Text>
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
<Box direction="Column" gap="100">
|
|
<Text size="L400">Options</Text>
|
|
<SequenceCard
|
|
className={SequenceCardStyle}
|
|
variant="SurfaceVariant"
|
|
direction="Column"
|
|
gap="400"
|
|
>
|
|
<SettingTile
|
|
title="Clear Cache & Reload"
|
|
description="Clear all your locally stored data and reload from server. This also deletes this device's encryption keys — older encrypted messages stay unreadable here unless key backup is set up."
|
|
after={
|
|
<Button
|
|
onClick={() => clearCacheAndReload(mx)}
|
|
variant="Secondary"
|
|
fill="Soft"
|
|
size="300"
|
|
radii="300"
|
|
outlined
|
|
>
|
|
<Text size="B300">Clear Cache</Text>
|
|
</Button>
|
|
}
|
|
/>
|
|
</SequenceCard>
|
|
</Box>
|
|
{(serverSupport || supportLoading) && (
|
|
<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 }}>
|
|
{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} direction="Column" gap="100">
|
|
<Text size="T300" priority="300">
|
|
{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 && (
|
|
<Box alignItems="Center" gap="200">
|
|
<Text size="T300" priority="300">
|
|
Support Page:
|
|
</Text>
|
|
<Text
|
|
as="a"
|
|
href={serverSupport.support_page}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
aria-label="Open support page"
|
|
size="T300"
|
|
>
|
|
{serverSupport.support_page}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</SequenceCard>
|
|
</Box>
|
|
)}
|
|
<Box direction="Column" gap="100">
|
|
<Text size="L400">Credits</Text>
|
|
<SequenceCard
|
|
className={SequenceCardStyle}
|
|
variant="SurfaceVariant"
|
|
direction="Column"
|
|
gap="400"
|
|
>
|
|
<Box
|
|
as="ul"
|
|
direction="Column"
|
|
gap="200"
|
|
style={{
|
|
margin: 0,
|
|
paddingLeft: config.space.S400,
|
|
}}
|
|
>
|
|
<li>
|
|
<Text size="T300">
|
|
The Lotus Chat logo is a derivative work based on the original{' '}
|
|
<a
|
|
href="https://github.com/cinnyapp/cinny"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
Cinny
|
|
</a>{' '}
|
|
logo by{' '}
|
|
<a
|
|
href="https://github.com/ajbura"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
Ajay Bura
|
|
</a>{' '}
|
|
and contributors, used under the terms of{' '}
|
|
<a
|
|
href="https://creativecommons.org/licenses/by/4.0/"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
CC-BY 4.0
|
|
</a>
|
|
. The modified logo is © Lotus Guild, also under CC-BY 4.0.
|
|
</Text>
|
|
</li>
|
|
<li>
|
|
<Text size="T300">
|
|
Lotus Chat is a fork of{' '}
|
|
<a
|
|
href="https://github.com/cinnyapp/cinny"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
Cinny
|
|
</a>{' '}
|
|
by Ajay Bura and contributors, used under the terms of{' '}
|
|
<a
|
|
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
AGPL-3.0
|
|
</a>
|
|
.
|
|
</Text>
|
|
</li>
|
|
<li>
|
|
<Text size="T300">
|
|
The{' '}
|
|
<a
|
|
href="https://github.com/matrix-org/matrix-js-sdk"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
matrix-js-sdk
|
|
</a>{' '}
|
|
is ©{' '}
|
|
<a
|
|
href="https://matrix.org/foundation"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
The Matrix.org Foundation C.I.C
|
|
</a>{' '}
|
|
used under the terms of{' '}
|
|
<a
|
|
href="http://www.apache.org/licenses/LICENSE-2.0"
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
>
|
|
Apache 2.0
|
|
</a>
|
|
.
|
|
</Text>
|
|
</li>
|
|
<li>
|
|
<Text size="T300">
|
|
The{' '}
|
|
<a
|
|
href="https://github.com/mozilla/twemoji-colr"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
twemoji-colr
|
|
</a>{' '}
|
|
font is ©{' '}
|
|
<a href="https://mozilla.org/" target="_blank" rel="noreferrer noopener">
|
|
Mozilla Foundation
|
|
</a>{' '}
|
|
used under the terms of{' '}
|
|
<a
|
|
href="http://www.apache.org/licenses/LICENSE-2.0"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
Apache 2.0
|
|
</a>
|
|
.
|
|
</Text>
|
|
</li>
|
|
<li>
|
|
<Text size="T300">
|
|
The{' '}
|
|
<a
|
|
href="https://twemoji.twitter.com"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
Twemoji
|
|
</a>{' '}
|
|
emoji art is ©{' '}
|
|
<a
|
|
href="https://twemoji.twitter.com"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
Twitter, Inc and other contributors
|
|
</a>{' '}
|
|
used under the terms of{' '}
|
|
<a
|
|
href="https://creativecommons.org/licenses/by/4.0/"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
CC-BY 4.0
|
|
</a>
|
|
.
|
|
</Text>
|
|
</li>
|
|
<li>
|
|
<Text size="T300">
|
|
The{' '}
|
|
<a
|
|
href="https://material.io/design/sound/sound-resources.html"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
Material sound resources
|
|
</a>{' '}
|
|
are ©{' '}
|
|
<a href="https://google.com" target="_blank" rel="noreferrer noopener">
|
|
Google
|
|
</a>{' '}
|
|
used under the terms of{' '}
|
|
<a
|
|
href="https://creativecommons.org/licenses/by/4.0/"
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
>
|
|
CC-BY 4.0
|
|
</a>
|
|
.
|
|
</Text>
|
|
</li>
|
|
</Box>
|
|
</SequenceCard>
|
|
</Box>
|
|
</Box>
|
|
</PageContent>
|
|
</Scroll>
|
|
</Box>
|
|
</Page>
|
|
);
|
|
}
|