Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

redesigned app settings and switch to rust crypto #1988

Merged
merged 58 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
97a0fed
rework general settings
ajbura Oct 1, 2024
5401b2e
account settings - WIP
ajbura Dec 6, 2024
285d8d1
add missing key prop
ajbura Dec 7, 2024
6fa0cfc
add object url hook
ajbura Dec 7, 2024
f67fbd1
extract wide modal styles
ajbura Dec 7, 2024
c43bbea
profile settings and image editor - WIP
ajbura Dec 7, 2024
c34c3e3
add outline style to upload card
ajbura Dec 14, 2024
fa199df
remove file param from bind upload atom hook
ajbura Dec 14, 2024
ff371ee
add compact variant to upload card
ajbura Dec 14, 2024
9c46a6f
add compact upload card renderer
ajbura Dec 14, 2024
de281ad
add option to update profile avatar
ajbura Dec 15, 2024
77111f8
add option to change profile displayname
ajbura Dec 15, 2024
a1b2e36
allow displayname change based on capabilities check
ajbura Dec 15, 2024
e8ace5f
rearrange settings components into folders
ajbura Dec 15, 2024
97ec996
add system notification settings
ajbura Dec 15, 2024
233ec01
add initial page param in settings
ajbura Dec 15, 2024
643da9e
convert account data hook to typescript
ajbura Dec 16, 2024
d3b2e73
add push rule hook
ajbura Dec 16, 2024
feec0a8
add notification mode hook
ajbura Dec 16, 2024
bda6198
add notification mode switcher component
ajbura Dec 16, 2024
a1791ca
add all messages notification settings options
ajbura Dec 16, 2024
a5042bb
Merge branch 'dev' into rework-user-settings
ajbura Dec 16, 2024
cb7db3c
add special messages notification settings
ajbura Dec 17, 2024
9a0d716
add keyword notifications
ajbura Dec 18, 2024
02a8577
add ignored users section
ajbura Dec 18, 2024
7c91dac
improve ignore user list strings
ajbura Dec 18, 2024
37524fa
add about settings
ajbura Dec 18, 2024
e685eb9
add access token option in about settings
ajbura Dec 18, 2024
ef8d09e
add developer tools settings
ajbura Dec 18, 2024
061f27a
add expand button to account data dev tool option
ajbura Dec 18, 2024
e949a5e
update folds
ajbura Dec 21, 2024
a91acbd
fix editable active element textarea check
ajbura Dec 21, 2024
6b1d827
do not close dialog when editable element in focus
ajbura Dec 21, 2024
6b75bc8
add text area plugins
ajbura Dec 21, 2024
81738d4
add text area intent handler hook
ajbura Dec 21, 2024
64a5bf5
add newline intent mod in text area
ajbura Dec 21, 2024
5029516
add next line hotkey in text area intent hook
ajbura Dec 21, 2024
f6b8b01
add syntax error position dom utility function
ajbura Dec 22, 2024
7d4c529
add account data editor
ajbura Dec 22, 2024
d6d22da
add button to send new account data in dev tools
ajbura Dec 22, 2024
3bfc80c
improve custom emoji plugin
ajbura Dec 26, 2024
15d72b6
add more custom emojis hooks
ajbura Dec 26, 2024
17727de
add text util css
ajbura Dec 26, 2024
c1ef616
add word break in setting tile title and description
ajbura Dec 26, 2024
9d812a6
emojis and sticker user settings - WIP
ajbura Dec 26, 2024
5bc1eab
view image packs from settings
ajbura Jan 4, 2025
b18bce0
emoji pack editing - WIP
ajbura Jan 5, 2025
77ebd1a
Merge branch 'dev' into rework-user-settings
ajbura Jan 5, 2025
e9d8a7b
add option to edit pack meta
ajbura Jan 12, 2025
c983e20
Merge branch 'dev' into rework-user-settings
ajbura Jan 12, 2025
bf31a8e
change saved changes message
ajbura Jan 12, 2025
632e54d
add image edit and delete controls
ajbura Jan 13, 2025
7b41fde
add option to upload pack images and apply changes
ajbura Jan 15, 2025
6ba0da9
fix state event type when updating image pack
ajbura Jan 15, 2025
f4bf351
lazy load pack image tile img
ajbura Jan 15, 2025
4618ab5
hide upload image button when user can not edit pack
ajbura Jan 15, 2025
9bb7319
add option to add or remove global image packs
ajbura Jan 16, 2025
39521b7
upgrade to rust crypto (#2168)
ajbura Feb 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,593 changes: 3,514 additions & 4,079 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"file-saver": "2.0.5",
"flux": "4.0.3",
"focus-trap-react": "10.0.2",
"folds": "2.0.0",
"folds": "2.1.0",
"formik": "2.4.6",
"html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0",
Expand All @@ -56,7 +56,7 @@
"jotai": "2.6.0",
"linkify-react": "4.1.3",
"linkifyjs": "4.1.3",
"matrix-js-sdk": "34.11.1",
"matrix-js-sdk": "35.0.0",
"millify": "6.1.0",
"pdfjs-dist": "4.2.67",
"prismjs": "1.29.0",
Expand Down Expand Up @@ -108,6 +108,6 @@
"vite": "5.0.13",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-static-copy": "1.0.4",
"vite-plugin-top-level-await": "1.4.1"
"vite-plugin-top-level-await": "1.4.4"
}
}
73 changes: 73 additions & 0 deletions src/app/components/ActionUIA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { ReactNode } from 'react';
import { AuthDict, AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
import { getUIAFlowForStages } from '../utils/matrix-uia';
import { useSupportedUIAFlows, useUIACompleted, useUIAFlow } from '../hooks/useUIAFlows';
import { UIAFlowOverlay } from './UIAFlowOverlay';
import { PasswordStage, SSOStage } from './uia-stages';
import { useMatrixClient } from '../hooks/useMatrixClient';

export const SUPPORTED_IN_APP_UIA_STAGES = [AuthType.Password, AuthType.Sso];

export const pickUIAFlow = (uiaFlows: UIAFlow[]): UIAFlow | undefined => {
const passwordFlow = getUIAFlowForStages(uiaFlows, [AuthType.Password]);
if (passwordFlow) return passwordFlow;
return getUIAFlowForStages(uiaFlows, [AuthType.Sso]);
};

type ActionUIAProps = {
authData: IAuthData;
ongoingFlow: UIAFlow;
action: (authDict: AuthDict) => void;
onCancel: () => void;
};
export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIAProps) {
const mx = useMatrixClient();
const completed = useUIACompleted(authData);
const { getStageToComplete } = useUIAFlow(authData, ongoingFlow);

const stageToComplete = getStageToComplete();

if (!stageToComplete) return null;
return (
<UIAFlowOverlay
currentStep={completed.length + 1}
stepCount={ongoingFlow.stages.length}
onCancel={onCancel}
>
{stageToComplete.type === AuthType.Password && (
<PasswordStage
userId={mx.getUserId()!}
stageData={stageToComplete}
onCancel={onCancel}
submitAuthDict={action}
/>
)}
{stageToComplete.type === AuthType.Sso && stageToComplete.session && (
<SSOStage
ssoRedirectURL={mx.getFallbackAuthUrl(AuthType.Sso, stageToComplete.session)}
stageData={stageToComplete}
onCancel={onCancel}
submitAuthDict={action}
/>
)}
</UIAFlowOverlay>
);
}

type ActionUIAFlowsLoaderProps = {
authData: IAuthData;
unsupported: () => ReactNode;
children: (ongoingFlow: UIAFlow) => ReactNode;
};
export function ActionUIAFlowsLoader({
authData,
unsupported,
children,
}: ActionUIAFlowsLoaderProps) {
const supportedFlows = useSupportedUIAFlows(authData.flows ?? [], SUPPORTED_IN_APP_UIA_STAGES);
const ongoingFlow = supportedFlows.length > 0 ? supportedFlows[0] : undefined;

if (!ongoingFlow) return unsupported();

return children(ongoingFlow);
}
281 changes: 281 additions & 0 deletions src/app/components/BackupRestore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import React, { MouseEventHandler, useCallback, useState } from 'react';
import { useAtom } from 'jotai';
import { CryptoApi, KeyBackupInfo } from 'matrix-js-sdk/lib/crypto-api';
import {
Badge,
Box,
Button,
color,
config,
Icon,
IconButton,
Icons,
Menu,
percent,
PopOut,
ProgressBar,
RectCords,
Spinner,
Text,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { BackupProgressStatus, backupRestoreProgressAtom } from '../state/backupRestore';
import { InfoCard } from './info-card';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import {
useKeyBackupInfo,
useKeyBackupStatus,
useKeyBackupSync,
useKeyBackupTrust,
} from '../hooks/useKeyBackup';
import { stopPropagation } from '../utils/keyboard';
import { useRestoreBackupOnVerification } from '../hooks/useRestoreBackupOnVerification';

type BackupStatusProps = {
enabled: boolean;
};
function BackupStatus({ enabled }: BackupStatusProps) {
return (
<Box as="span" gap="100" alignItems="Center">
<Badge variant={enabled ? 'Success' : 'Critical'} fill="Solid" size="200" radii="Pill" />
<Text
as="span"
size="L400"
style={{ color: enabled ? color.Success.Main : color.Critical.Main }}
>
{enabled ? 'Connected' : 'Disconnected'}
</Text>
</Box>
);
}
type BackupSyncingProps = {
count: number;
};
function BackupSyncing({ count }: BackupSyncingProps) {
return (
<Box as="span" gap="100" alignItems="Center">
<Spinner size="50" variant="Primary" fill="Soft" />
<Text as="span" size="L400" style={{ color: color.Primary.Main }}>
Syncing ({count})
</Text>
</Box>
);
}

function BackupProgressFetching() {
return (
<Box grow="Yes" gap="200" alignItems="Center">
<Badge variant="Secondary" fill="Solid" radii="300">
<Text size="L400">Restoring: 0%</Text>
</Badge>
<Box grow="Yes" direction="Column">
<ProgressBar variant="Secondary" size="300" min={0} max={1} value={0} />
</Box>
<Spinner size="50" variant="Secondary" fill="Soft" />
</Box>
);
}

type BackupProgressProps = {
total: number;
downloaded: number;
};
function BackupProgress({ total, downloaded }: BackupProgressProps) {
return (
<Box grow="Yes" gap="200" alignItems="Center">
<Badge variant="Secondary" fill="Solid" radii="300">
<Text size="L400">Restoring: {`${Math.round(percent(0, total, downloaded))}%`}</Text>
</Badge>
<Box grow="Yes" direction="Column">
<ProgressBar variant="Secondary" size="300" min={0} max={total} value={downloaded} />
</Box>
<Badge variant="Secondary" fill="Soft" radii="Pill">
<Text size="L400">
{downloaded} / {total}
</Text>
</Badge>
</Box>
);
}

type BackupTrustInfoProps = {
crypto: CryptoApi;
backupInfo: KeyBackupInfo;
};
function BackupTrustInfo({ crypto, backupInfo }: BackupTrustInfoProps) {
const trust = useKeyBackupTrust(crypto, backupInfo);

if (!trust) return null;

return (
<Box direction="Column">
{trust.matchesDecryptionKey ? (
<Text size="T200" style={{ color: color.Success.Main }}>
<b>Backup has trusted decryption key.</b>
</Text>
) : (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>Backup does not have trusted decryption key!</b>
</Text>
)}
{trust.trusted ? (
<Text size="T200" style={{ color: color.Success.Main }}>
<b>Backup has trusted by signature.</b>
</Text>
) : (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>Backup does not have trusted signature!</b>
</Text>
)}
</Box>
);
}

type BackupRestoreTileProps = {
crypto: CryptoApi;
};
export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
const [restoreProgress, setRestoreProgress] = useAtom(backupRestoreProgressAtom);
const restoring =
restoreProgress.status === BackupProgressStatus.Fetching ||
restoreProgress.status === BackupProgressStatus.Loading;

const backupEnabled = useKeyBackupStatus(crypto);
const backupInfo = useKeyBackupInfo(crypto);
const [remainingSession, syncFailure] = useKeyBackupSync();

const [menuCords, setMenuCords] = useState<RectCords>();

const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCords(evt.currentTarget.getBoundingClientRect());
};

const [restoreState, restoreBackup] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await crypto.restoreKeyBackup({
progressCallback(progress) {
setRestoreProgress(progress);
},
});
}, [crypto, setRestoreProgress])
);

const handleRestore = () => {
setMenuCords(undefined);
restoreBackup();
};

return (
<InfoCard
variant="Surface"
title="Encryption Backup"
after={
<Box alignItems="Center" gap="200">
{remainingSession === 0 ? (
<BackupStatus enabled={backupEnabled} />
) : (
<BackupSyncing count={remainingSession} />
)}
<IconButton
aria-pressed={!!menuCords}
size="300"
variant="Surface"
radii="300"
onClick={handleMenu}
>
<Icon size="100" src={Icons.VerticalDots} />
</IconButton>
<PopOut
anchor={menuCords}
offset={5}
position="Bottom"
align="End"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setMenuCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
isKeyBackward: (evt: KeyboardEvent) =>
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
escapeDeactivates: stopPropagation,
}}
>
<Menu
style={{
padding: config.space.S100,
}}
>
<Box direction="Column" gap="100">
<Box direction="Column" gap="200">
<InfoCard
variant="SurfaceVariant"
title="Backup Details"
description={
<>
<span>Version: {backupInfo?.version ?? 'NIL'}</span>
<br />
<span>Keys: {backupInfo?.count ?? 'NIL'}</span>
</>
}
/>
</Box>
<Button
size="300"
variant="Success"
radii="300"
aria-disabled={restoreState.status === AsyncStatus.Loading || restoring}
onClick={
restoreState.status === AsyncStatus.Loading || restoring
? undefined
: handleRestore
}
before={<Icon size="100" src={Icons.Download} />}
>
<Text size="B300">Restore Backup</Text>
</Button>
</Box>
</Menu>
</FocusTrap>
}
/>
</Box>
}
>
{syncFailure && (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>{syncFailure}</b>
</Text>
)}
{!backupEnabled && backupInfo === null && (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>No backup present on server!</b>
</Text>
)}
{!syncFailure && !backupEnabled && backupInfo && (
<BackupTrustInfo crypto={crypto} backupInfo={backupInfo} />
)}
{restoreState.status === AsyncStatus.Loading && !restoring && <BackupProgressFetching />}
{restoreProgress.status === BackupProgressStatus.Fetching && <BackupProgressFetching />}
{restoreProgress.status === BackupProgressStatus.Loading && (
<BackupProgress
total={restoreProgress.data.total}
downloaded={restoreProgress.data.downloaded}
/>
)}
{restoreState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
<b>{restoreState.error.message}</b>
</Text>
)}
</InfoCard>
);
}

export function AutoRestoreBackupOnVerification() {
useRestoreBackupOnVerification();

return null;
}
2 changes: 1 addition & 1 deletion src/app/components/CapabilitiesAndMediaConfigLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function CapabilitiesAndMediaConfigLoader({
[]
>(
useCallback(async () => {
const result = await Promise.allSettled([mx.getCapabilities(true), mx.getMediaConfig()]);
const result = await Promise.allSettled([mx.getCapabilities(), mx.getMediaConfig()]);
const capabilities = promiseFulfilledResult(result[0]);
const mediaConfig = promiseFulfilledResult(result[1]);
return [capabilities, mediaConfig];
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/CapabilitiesLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type CapabilitiesLoaderProps = {
export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
const mx = useMatrixClient();

const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(true), [mx]));
const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(), [mx]));

useEffect(() => {
load();
Expand Down
Loading