redesigned app settings and switch to rust crypto (#1988)

* rework general settings

* account settings - WIP

* add missing key prop

* add object url hook

* extract wide modal styles

* profile settings and image editor - WIP

* add outline style to upload card

* remove file param from bind upload atom hook

* add compact variant to upload card

* add  compact upload card renderer

* add option to update profile avatar

* add option to change profile displayname

* allow displayname change based on capabilities check

* rearrange settings components into folders

* add system notification settings

* add initial page param in settings

* convert account data hook to typescript

* add push rule hook

* add notification mode hook

* add notification mode switcher component

* add all messages notification settings options

* add special messages notification settings

* add keyword notifications

* add ignored users section

* improve ignore user list strings

* add about settings

* add access token option in about settings

* add developer tools settings

* add expand button to account data dev tool option

* update folds

* fix editable active element textarea check

* do not close dialog when editable element in focus

* add text area plugins

* add text area intent handler hook

* add newline intent mod in text area

* add next line hotkey in text area intent hook

* add syntax error position dom utility function

* add account data editor

* add button to send new account data in dev tools

* improve custom emoji plugin

* add more custom emojis hooks

* add text util css

* add word break in setting tile title and description

* emojis and sticker user settings - WIP

* view image packs from settings

* emoji pack editing - WIP

* add option to edit pack meta

* change saved changes message

* add image edit and delete controls

* add option to upload pack images and apply changes

* fix state event type when updating image pack

* lazy load pack image tile img

* hide upload image button when user can not edit pack

* add option to add or remove global image packs

* upgrade to rust crypto (#2168)

* update matrix js sdk

* remove dead code

* use rust crypto

* update setPowerLevel usage

* fix types

* fix deprecated isRoomEncrypted method uses

* fix deprecated room.currentState uses

* fix deprecated import/export room keys func

* fix merge issues in image pack file

* fix remaining issues in image pack file

* start indexedDBStore

* update package lock and vite-plugin-top-level-await

* user session settings - WIP

* add useAsync hook

* add password stage uia

* add uia flow matrix error hook

* add UIA action component

* add options to delete sessions

* add sso uia stage

* fix SSO stage complete error

* encryption - WIP

* update user settings encryption terminology

* add default variant to password input

* use password input in uia password stage

* add options for local backup in user settings

* remove typo in import local backup password input label

* online backup - WIP

* fix uia sso action

* move access token settings from about to developer tools

* merge encryption tab into sessions and rename it to devices

* add device placeholder tile

* add logout dialog

* add logout button for current device

* move other devices in component

* render unverified device verification tile

* add learn more section for current device verification

* add device verification status badge

* add info card component

* add index file for password input component

* add types for secret storage

* add component to access secret storage key

* manual verification - WIP

* update matrix-js-sdk to v35

* add manual verification

* use react query for device list

* show unverified tab on sidebar

* fix device list updates

* add session key details to current device

* render restore encryption backup

* fix loading state of restore backup

* fix unverified tab settings closes after verification

* key backup tile - WIP

* fix unverified tab badge

* rename session key to device key in device tile

* improve backup restore functionality

* fix restore button enabled after layout reload during restoring backup

* update backup info on status change

* add backup disconnection failures

* add device verification using sas

* restore backup after verification

* show option to logout on startup error screen

* fix key backup hook update on decryption key cached

* add option to enable device verification

* add device verification reset dialog

* add logout button in settings drawer

* add encrypted message lost on logout

* fix backup restore never finish with 0 keys

* fix setup dialog hides when enabling device verification

* show backup details in menu

* update setup device verification body copy

* replace deprecated method

* fix displayname appear as mxid in settings

* remove old refactored codes

* fix types
This commit is contained in:
Ajay Bura 2025-02-10 16:49:47 +11:00 committed by GitHub
parent f5d68fcc22
commit 56b754153a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
196 changed files with 14171 additions and 8403 deletions

View file

@ -18,6 +18,7 @@ import FocusTrap from 'focus-trap-react';
import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useState } from 'react';
import {
clearCacheAndReload,
clearLoginData,
initClient,
logoutClient,
startClient,
@ -48,7 +49,7 @@ function ClientRootLoading() {
);
}
function ClientRootOptions({ mx }: { mx: MatrixClient }) {
function ClientRootOptions({ mx }: { mx?: MatrixClient }) {
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const handleToggle: MouseEventHandler<HTMLButtonElement> = (evt) => {
@ -90,13 +91,21 @@ function ClientRootOptions({ mx }: { mx: MatrixClient }) {
>
<Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
<Text as="span" size="T300" truncate>
Clear Cache and Reload
</Text>
</MenuItem>
{mx && (
<MenuItem onClick={() => clearCacheAndReload(mx)} size="300" radii="300">
<Text as="span" size="T300" truncate>
Clear Cache and Reload
</Text>
</MenuItem>
)}
<MenuItem
onClick={() => logoutClient(mx)}
onClick={() => {
if (mx) {
logoutClient(mx);
return;
}
clearLoginData();
}}
size="300"
radii="300"
variant="Critical"
@ -172,7 +181,7 @@ export function ClientRoot({ children }: ClientRootProps) {
return (
<SpecVersions baseUrl={baseUrl!}>
{mx && <SyncStatus mx={mx} />}
{loading && mx && <ClientRootOptions mx={mx} />}
{loading && <ClientRootOptions mx={mx} />}
{(loadState.status === AsyncStatus.Error || startState.status === AsyncStatus.Error) && (
<SplashScreen>
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="400">
@ -182,7 +191,7 @@ export function ClientRoot({ children }: ClientRootProps) {
<Text>{`Failed to load. ${loadState.error.message}`}</Text>
)}
{startState.status === AsyncStatus.Error && (
<Text>{`Failed to load. ${startState.error.message}`}</Text>
<Text>{`Failed to start. ${startState.error.message}`}</Text>
)}
<Button variant="Critical" onClick={mx ? () => startMatrix(mx) : loadMatrix}>
<Text as="span" size="B400">

View file

@ -16,7 +16,7 @@ import {
SpaceTabs,
InboxTab,
ExploreTab,
UserTab,
SettingsTab,
UnverifiedTab,
} from './sidebar';
import { openCreateRoom, openSearch } from '../../../client/action/navigation';
@ -76,7 +76,7 @@ export function SidebarNav() {
<UnverifiedTab />
<InboxTab />
<UserTab />
<SettingsTab />
</SidebarStack>
</>
}

View file

@ -0,0 +1,49 @@
import React, { useState } from 'react';
import { Text } from 'folds';
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
import { UserAvatar } from '../../../components/user-avatar';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { nameInitials } from '../../../utils/common';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { Settings } from '../../../features/settings';
import { useUserProfile } from '../../../hooks/useUserProfile';
import { Modal500 } from '../../../components/Modal500';
export function SettingsTab() {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const userId = mx.getUserId()!;
const profile = useUserProfile(userId);
const [settings, setSettings] = useState(false);
const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
const avatarUrl = profile.avatarUrl
? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
const openSettings = () => setSettings(true);
const closeSettings = () => setSettings(false);
return (
<SidebarItem active={settings}>
<SidebarItemTooltip tooltip={displayName}>
{(triggerRef) => (
<SidebarAvatar as="button" ref={triggerRef} onClick={openSettings}>
<UserAvatar
userId={userId}
src={avatarUrl}
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
/>
</SidebarAvatar>
)}
</SidebarItemTooltip>
{settings && (
<Modal500 requestClose={closeSettings}>
<Settings requestClose={closeSettings} />
</Modal500>
)}
</SidebarItem>
);
}

View file

@ -22,3 +22,9 @@ export const UnverifiedAvatar = style({
color: color.Critical.OnContainer,
borderColor: color.Critical.ContainerLine,
});
export const UnverifiedOtherAvatar = style({
backgroundColor: color.Warning.Container,
color: color.Warning.OnContainer,
borderColor: color.Warning.ContainerLine,
});

View file

@ -1,49 +1,94 @@
import React from 'react';
import React, { useState } from 'react';
import { Badge, color, Icon, Icons, Text } from 'folds';
import { openSettings } from '../../../../client/action/navigation';
import { isCrossVerified } from '../../../../util/matrixUtil';
import {
SidebarAvatar,
SidebarItem,
SidebarItemBadge,
SidebarItemTooltip,
} from '../../../components/sidebar';
import { useDeviceList } from '../../../hooks/useDeviceList';
import { tabText } from '../../../organisms/settings/Settings';
import { useDeviceIds, useDeviceList, useSplitCurrentDevice } from '../../../hooks/useDeviceList';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import * as css from './UnverifiedTab.css';
import {
useDeviceVerificationStatus,
useUnverifiedDeviceCount,
VerificationStatus,
} from '../../../hooks/useDeviceVerificationStatus';
import { useCrossSigningActive } from '../../../hooks/useCrossSigning';
import { Modal500 } from '../../../components/Modal500';
import { Settings, SettingsPages } from '../../../features/settings';
export function UnverifiedTab() {
function UnverifiedIndicator() {
const mx = useMatrixClient();
const deviceList = useDeviceList();
const unverified = deviceList?.filter(
(device) => isCrossVerified(mx, device.device_id) === false
const crypto = mx.getCrypto();
const [devices] = useDeviceList();
const [currentDevice, otherDevices] = useSplitCurrentDevice(devices);
const verificationStatus = useDeviceVerificationStatus(
crypto,
mx.getSafeUserId(),
currentDevice?.device_id
);
const unverified = verificationStatus === VerificationStatus.Unverified;
const otherDevicesId = useDeviceIds(otherDevices);
const unverifiedDeviceCount = useUnverifiedDeviceCount(
crypto,
mx.getSafeUserId(),
otherDevicesId
);
if (!unverified?.length) return null;
const [settings, setSettings] = useState(false);
const closeSettings = () => setSettings(false);
const hasUnverified =
unverified || (unverifiedDeviceCount !== undefined && unverifiedDeviceCount > 0);
return (
<SidebarItem className={css.UnverifiedTab}>
<SidebarItemTooltip tooltip="Unverified Sessions">
{(triggerRef) => (
<SidebarAvatar
className={css.UnverifiedAvatar}
as="button"
ref={triggerRef}
outlined
onClick={() => openSettings(tabText.SECURITY)}
>
<Icon style={{ color: color.Critical.Main }} src={Icons.ShieldUser} />
</SidebarAvatar>
)}
</SidebarItemTooltip>
<SidebarItemBadge hasCount>
<Badge variant="Critical" size="400" fill="Solid" radii="Pill" outlined={false}>
<Text as="span" size="L400">
{unverified.length}
</Text>
</Badge>
</SidebarItemBadge>
</SidebarItem>
<>
{hasUnverified && (
<SidebarItem active={settings} className={css.UnverifiedTab}>
<SidebarItemTooltip tooltip={unverified ? 'Unverified Device' : 'Unverified Devices'}>
{(triggerRef) => (
<SidebarAvatar
className={unverified ? css.UnverifiedAvatar : css.UnverifiedOtherAvatar}
as="button"
ref={triggerRef}
outlined
onClick={() => setSettings(true)}
>
<Icon
style={{ color: unverified ? color.Critical.Main : color.Warning.Main }}
src={Icons.ShieldUser}
/>
</SidebarAvatar>
)}
</SidebarItemTooltip>
{!unverified && unverifiedDeviceCount && unverifiedDeviceCount > 0 && (
<SidebarItemBadge hasCount>
<Badge variant="Warning" size="400" fill="Solid" radii="Pill" outlined={false}>
<Text as="span" size="L400">
{unverifiedDeviceCount}
</Text>
</Badge>
</SidebarItemBadge>
)}
</SidebarItem>
)}
{settings && (
<Modal500 requestClose={closeSettings}>
<Settings initialPage={SettingsPages.DevicesPage} requestClose={closeSettings} />
</Modal500>
)}
</>
);
}
export function UnverifiedTab() {
const crossSigningActive = useCrossSigningActive();
if (!crossSigningActive) return null;
return <UnverifiedIndicator />;
}

View file

@ -1,65 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Text } from 'folds';
import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
import { openSettings } from '../../../../client/action/navigation';
import { UserAvatar } from '../../../components/user-avatar';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
import { nameInitials } from '../../../utils/common';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
type UserProfile = {
avatar_url?: string;
displayname?: string;
};
export function UserTab() {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const userId = mx.getUserId()!;
const [profile, setProfile] = useState<UserProfile>({});
const displayName = profile.displayname ?? getMxIdLocalPart(userId) ?? userId;
const avatarUrl = profile.avatar_url
? mxcUrlToHttp(mx, profile.avatar_url, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
useEffect(() => {
const user = mx.getUser(userId);
const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => {
setProfile((cp) => ({
...cp,
avatar_url: myUser.avatarUrl,
}));
};
const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => {
setProfile((cp) => ({
...cp,
avatar_url: myUser.displayName,
}));
};
mx.getProfileInfo(userId).then((info) => setProfile(() => ({ ...info })));
user?.on(UserEvent.AvatarUrl, onAvatarChange);
user?.on(UserEvent.DisplayName, onDisplayNameChange);
return () => {
user?.removeListener(UserEvent.AvatarUrl, onAvatarChange);
user?.removeListener(UserEvent.DisplayName, onDisplayNameChange);
};
}, [mx, userId]);
return (
<SidebarItem>
<SidebarItemTooltip tooltip="User Settings">
{(triggerRef) => (
<SidebarAvatar as="button" ref={triggerRef} onClick={() => openSettings()}>
<UserAvatar
userId={userId}
src={avatarUrl}
renderFallback={() => <Text size="H4">{nameInitials(displayName)}</Text>}
/>
</SidebarAvatar>
)}
</SidebarItemTooltip>
</SidebarItem>
);
}

View file

@ -3,5 +3,5 @@ export * from './DirectTab';
export * from './SpaceTabs';
export * from './InboxTab';
export * from './ExploreTab';
export * from './UserTab';
export * from './SettingsTab';
export * from './UnverifiedTab';

View file

@ -23,7 +23,8 @@ import {
toRem,
} from 'folds';
import { useVirtualizer } from '@tanstack/react-virtual';
import { IJoinRuleEventContent, JoinRule, Room } from 'matrix-js-sdk';
import { JoinRule, Room } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import FocusTrap from 'focus-trap-react';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { mDirectAtom } from '../../../state/mDirectList';
@ -201,7 +202,7 @@ function SpaceHeader() {
const joinRules = useStateEvent(
space,
StateEvent.RoomJoinRules
)?.getContent<IJoinRuleEventContent>();
)?.getContent<RoomJoinRulesEventContent>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
const cords = evt.currentTarget.getBoundingClientRect();