Files
cinny/src/app/features/settings/about/About.tsx
litruv 7d866404b3
All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 2s
Fix Android updates dialog, mobile About page, and user color crash guard.
Use stripBase for update static copy paths, show Settings detail on mobile
when nav is hidden, pause Settings focus trap during release notes, and guard
extractMemberColorPreference when room is not a Matrix Room instance.
2026-08-23 16:18:35 +10:00

382 lines
14 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react';
import { getAppVersion } from '../../../utils/appVersion';
import { useOpenReleaseNotesDialog } from '../../../components/updates-dialog/UpdatesDialogHost';
import { getCurrentUpdatePreview } from '../../../data/updateNotes';
import { Box, Text, IconButton, Scroll, Button, config, toRem } from 'folds';
import { Icon, Icons } from '../../../components/icons';
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 PaarrotSVG from '../../../../../public/res/svg/paarrot.svg';
import { clearCacheAndReload } from '../../../../client/initMatrix';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
type AboutProps = {
requestClose: () => void;
};
export function About({ requestClose }: AboutProps) {
const mx = useMatrixClient();
const openReleaseNotes = useOpenReleaseNotesDialog();
const [version, setVersion] = useState<string>('');
const [protocolStatus, setProtocolStatus] = useState<string>('Checking desktop protocol integration...');
const [protocolBusy, setProtocolBusy] = useState<boolean>(false);
const [updatePreview, setUpdatePreview] = useState<{ title: string; description: string } | null>(
null
);
const formatProtocolStatus = useCallback((data: {
scheme: string;
platform: string;
isDefault: boolean;
registerCall: boolean;
error: string | null;
windows: null | {
userChoiceProgId: string | null;
userChoiceCommand: string | null;
hkcuCommand: string | null;
hklmCommand: string | null;
};
}): string => {
const state = data.isDefault ? 'registered' : 'not registered';
const startupState = data.registerCall ? 'ok' : 'no-change';
const owner =
data.platform === 'win32' && data.windows?.userChoiceProgId
? ` Current default ProgId=${data.windows.userChoiceProgId}.`
: '';
const missingHint =
data.platform === 'win32' && !data.windows?.hkcuCommand && !data.windows?.hklmCommand
? ` ${data.scheme} link type is missing from Windows registry. Use Repair to create it.`
: '';
const errorPart = data.error ? ` Error: ${data.error}` : '';
return `${data.scheme} is ${state} (startup=${startupState}) on ${data.platform}.${owner}${missingHint}${errorPart}`;
}, []);
const refreshProtocolStatus = useCallback(() => {
if (!window.electron?.protocol?.getStatus) {
setProtocolStatus('Desktop protocol diagnostics are unavailable in this environment.');
return;
}
window.electron.protocol
.getStatus()
.then((result) => {
if (!result?.success || !result.data) {
setProtocolStatus(result?.error ?? 'Unable to query protocol state.');
return;
}
setProtocolStatus(formatProtocolStatus(result.data));
})
.catch((error) => {
setProtocolStatus(`Protocol diagnostics failed: ${error instanceof Error ? error.message : String(error)}`);
});
}, [formatProtocolStatus]);
const repairProtocolHandler = () => {
if (!window.electron?.protocol?.repair) {
setProtocolStatus('Protocol repair is unavailable in this environment.');
return;
}
setProtocolBusy(true);
window.electron.protocol
.repair()
.then((result) => {
if (!result?.success || !result.data) {
setProtocolStatus(result?.error ?? 'Protocol repair failed.');
return;
}
setProtocolStatus(formatProtocolStatus(result.data));
setProtocolBusy(false);
})
.catch((error) => {
setProtocolStatus(`Protocol repair failed: ${error instanceof Error ? error.message : String(error)}`);
setProtocolBusy(false);
});
};
useEffect(() => {
getAppVersion().then(setVersion).catch(() => setVersion('unknown'));
refreshProtocolStatus();
getCurrentUpdatePreview().then(setUpdatePreview).catch(() => setUpdatePreview(null));
}, [refreshProtocolStatus]);
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">
<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={PaarrotSVG}
alt="Paarrot logo"
/>
</Box>
<Box direction="Column" gap="300">
<Box direction="Column" gap="100">
<Box gap="100" alignItems="End">
<Text size="H3">Paarrot</Text>
<Text size="T200">v{version}</Text>
</Box>
<Text>Squawk to yer mateys!</Text>
</Box>
<Box gap="200" wrap="Wrap">
<Button
as="a"
href="https://github.com/Paarrot/Paarrot-Desktop"
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>
</Box>
</Box>
</Box>
<Box direction="Column" gap="100">
<Text size="L400">What&apos;s new</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="300"
>
<SettingTile
title={updatePreview?.title ?? 'Release notes'}
description={
updatePreview
? `Paarrot ${version}${updatePreview.description}`
: 'View highlights from the latest Paarrot update.'
}
after={
<Button
onClick={openReleaseNotes}
variant="Secondary"
fill="Soft"
size="300"
radii="300"
outlined
>
<Text size="B300">View updates</Text>
</Button>
}
/>
</SequenceCard>
</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."
after={
<Button
onClick={() => clearCacheAndReload(mx)}
variant="Secondary"
fill="Soft"
size="300"
radii="300"
outlined
>
<Text size="B300">Clear Cache</Text>
</Button>
}
/>
<SettingTile
title="Protocol Handler (paarrot://)"
description={protocolStatus}
after={
<Box gap="100">
<Button
onClick={repairProtocolHandler}
variant="Secondary"
fill="Soft"
size="300"
radii="300"
outlined
disabled={protocolBusy}
>
<Text size="B300">Repair</Text>
</Button>
<Button
onClick={refreshProtocolStatus}
variant="Secondary"
fill="Soft"
size="300"
radii="300"
outlined
disabled={protocolBusy}
>
<Text size="B300">Refresh</Text>
</Button>
</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{' '}
<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>
);
}