merge encryption tab into sessions and rename it to devices

This commit is contained in:
Ajay Bura 2025-01-26 20:58:24 +05:30
parent 6def5ab17e
commit a50b6efd46
9 changed files with 262 additions and 338 deletions

View file

@ -11,8 +11,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { UserAvatar } from '../../components/user-avatar';
import { nameInitials } from '../../utils/common';
import { Notifications } from './notifications';
import { Sessions } from './sessions';
import { Encryption } from './encryption';
import { Devices } from './devices';
import { EmojisStickers } from './emojis-stickers';
import { DeveloperTools } from './developer-tools';
import { About } from './about';
@ -21,8 +20,7 @@ enum SettingsPages {
GeneralPage,
AccountPage,
NotificationPage,
SessionsPage,
EncryptionPage,
DevicesPage,
EmojisStickersPage,
DeveloperToolsPage,
AboutPage,
@ -53,15 +51,10 @@ const useSettingsMenuItems = (): SettingsMenuItem[] =>
icon: Icons.Bell,
},
{
page: SettingsPages.SessionsPage,
name: 'Sessions',
page: SettingsPages.DevicesPage,
name: 'Devices',
icon: Icons.Category,
},
{
page: SettingsPages.EncryptionPage,
name: 'Encryption',
icon: Icons.ShieldLock,
},
{
page: SettingsPages.EmojisStickersPage,
name: 'Emojis & Stickers',
@ -173,11 +166,8 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
{activePage === SettingsPages.NotificationPage && (
<Notifications requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.SessionsPage && (
<Sessions requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.EncryptionPage && (
<Encryption requestClose={handlePageRequestClose} />
{activePage === SettingsPages.DevicesPage && (
<Devices requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.EmojisStickersPage && (
<EmojisStickers requestClose={handlePageRequestClose} />

View file

@ -0,0 +1,223 @@
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import { Box, Text, IconButton, Icon, Icons, Chip, Input, Button, color, Spinner } from 'folds';
import { IMyDevice, MatrixError } from 'matrix-js-sdk';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../../utils/time';
import { BreakWord } from '../../../styles/Text.css';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
function DeviceActiveTime({ ts }: { ts: number }) {
return (
<Text className={BreakWord} size="T200">
<Text size="Inherit" as="span" priority="300">
{'Last activity: '}
</Text>
<>
{today(ts) && 'Today'}
{yesterday(ts) && 'Yesterday'}
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
</>
</Text>
);
}
function DeviceDetails({ device }: { device: IMyDevice }) {
return (
<>
{typeof device.device_id === 'string' && (
<Text className={BreakWord} size="T200" priority="300">
Device ID: <i>{device.device_id}</i>
</Text>
)}
{typeof device.last_seen_ip === 'string' && (
<Text className={BreakWord} size="T200" priority="300">
IP Address: <i>{device.last_seen_ip}</i>
</Text>
)}
</>
);
}
type DeviceRenameProps = {
device: IMyDevice;
onCancel: () => void;
onRename: () => void;
refreshDeviceList: () => Promise<void>;
};
function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceRenameProps) {
const mx = useMatrixClient();
const [renameState, rename] = useAsyncCallback<void, MatrixError, [string]>(
useCallback(
async (name: string) => {
await mx.setDeviceDetails(device.device_id, { display_name: name });
await refreshDeviceList();
},
[mx, device.device_id, refreshDeviceList]
)
);
const renaming = renameState.status === AsyncStatus.Loading;
useEffect(() => {
if (renameState.status === AsyncStatus.Success) {
onRename();
}
}, [renameState, onRename]);
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
if (renaming) return;
const target = evt.target as HTMLFormElement | undefined;
const nameInput = target?.nameInput as HTMLInputElement | undefined;
if (!nameInput) return;
const deviceName = nameInput.value.trim();
if (!deviceName || deviceName === device.display_name) return;
rename(deviceName);
};
return (
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
<Text size="L400">Device Name</Text>
<Box gap="200">
<Box grow="Yes" direction="Column">
<Input
name="nameInput"
size="300"
variant="Secondary"
radii="300"
defaultValue={device.display_name}
autoFocus
required
readOnly={renaming}
/>
</Box>
<Box shrink="No" gap="200">
<Button
type="submit"
size="300"
variant="Success"
radii="300"
fill="Solid"
disabled={renaming}
before={renaming && <Spinner size="100" variant="Success" fill="Solid" />}
>
<Text size="B300">Save</Text>
</Button>
<Button
type="button"
size="300"
variant="Secondary"
radii="300"
fill="Soft"
onClick={onCancel}
disabled={renaming}
>
<Text size="B300">Cancel</Text>
</Button>
</Box>
</Box>
{renameState.status === AsyncStatus.Error ? (
<Text size="T200" style={{ color: color.Critical.Main }}>
{renameState.error.message}
</Text>
) : (
<Text size="T200">Device names are visible to public.</Text>
)}
</Box>
);
}
type DeviceTileProps = {
device: IMyDevice;
deleted: boolean;
onDeleteToggle: (deviceId: string) => void;
refreshDeviceList: () => Promise<void>;
disabled?: boolean;
};
export function DeviceTile({
device,
deleted,
onDeleteToggle,
refreshDeviceList,
disabled,
}: DeviceTileProps) {
const activeTs = device.last_seen_ts;
const [details, setDetails] = useState(false);
const [edit, setEdit] = useState(false);
const handleRename = useCallback(() => {
setEdit(false);
}, []);
return (
<>
<SettingTile
before={
<IconButton
variant={deleted ? 'Critical' : 'Secondary'}
outlined={deleted}
radii="300"
onClick={() => setDetails(!details)}
>
<Icon size="50" src={details ? Icons.ChevronBottom : Icons.ChevronRight} />
</IconButton>
}
after={
!edit && (
<Box shrink="No" alignItems="Center" gap="200">
{deleted ? (
<Chip
variant="Critical"
fill="None"
radii="Pill"
onClick={() => onDeleteToggle?.(device.device_id)}
disabled={disabled}
>
<Text size="B300">Undo</Text>
</Chip>
) : (
<>
<Chip
variant="Secondary"
fill="None"
radii="Pill"
onClick={() => onDeleteToggle?.(device.device_id)}
disabled={disabled}
>
<Icon size="50" src={Icons.Delete} />
</Chip>
<Chip
variant="Secondary"
radii="Pill"
onClick={() => setEdit(true)}
disabled={disabled}
>
<Text size="B300">Edit</Text>
</Chip>
</>
)}
</Box>
)
}
>
<Text size="T300">{device.display_name ?? device.device_id}</Text>
<Box direction="Column">
{typeof activeTs === 'number' && <DeviceActiveTime ts={activeTs} />}
{details && <DeviceDetails device={device} />}
</Box>
</SettingTile>
{edit && (
<DeviceRename
device={device}
onCancel={() => setEdit(false)}
onRename={handleRename}
refreshDeviceList={refreshDeviceList}
/>
)}
</>
);
}

View file

@ -1,4 +1,4 @@
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import React, { useCallback, useState } from 'react';
import {
Box,
Text,
@ -6,32 +6,24 @@ import {
Icon,
Icons,
Scroll,
Chip,
Input,
Button,
color,
Spinner,
toRem,
Menu,
config,
} from 'folds';
import { AuthDict, IMyDevice, MatrixError } from 'matrix-js-sdk';
import { AuthDict, MatrixError } from 'matrix-js-sdk';
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 { useDeviceList } from '../../../hooks/useDeviceList';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { timeDayMonYear, timeHourMinute, today, yesterday } from '../../../utils/time';
import { BreakWord } from '../../../styles/Text.css';
import {
AsyncState,
AsyncStatus,
useAsync,
useAsyncCallback,
} from '../../../hooks/useAsyncCallback';
import { AsyncState, AsyncStatus, useAsync } from '../../../hooks/useAsyncCallback';
import { ActionUIA, ActionUIAFlowsLoader } from '../../../components/ActionUIA';
import { useUIAMatrixError } from '../../../hooks/useUIAFlows';
import { LocalBackup } from './LocalBackup';
import { DeviceTile } from './DeviceTile';
function DevicesPlaceholder() {
return (
@ -75,225 +67,10 @@ function DevicesPlaceholder() {
);
}
function DeviceActiveTime({ ts }: { ts: number }) {
return (
<Text className={BreakWord} size="T200">
<Text size="Inherit" as="span" priority="300">
{'Last activity: '}
</Text>
<>
{today(ts) && 'Today'}
{yesterday(ts) && 'Yesterday'}
{!today(ts) && !yesterday(ts) && timeDayMonYear(ts)} {timeHourMinute(ts)}
</>
</Text>
);
}
function DeviceDetails({ device }: { device: IMyDevice }) {
return (
<>
{typeof device.device_id === 'string' && (
<Text className={BreakWord} size="T200" priority="300">
Session ID: <i>{device.device_id}</i>
</Text>
)}
{typeof device.last_seen_ip === 'string' && (
<Text className={BreakWord} size="T200" priority="300">
IP Address: <i>{device.last_seen_ip}</i>
</Text>
)}
</>
);
}
type DeviceRenameProps = {
device: IMyDevice;
onCancel: () => void;
onRename: () => void;
refreshDeviceList: () => Promise<void>;
};
function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceRenameProps) {
const mx = useMatrixClient();
const [renameState, rename] = useAsyncCallback<void, MatrixError, [string]>(
useCallback(
async (name: string) => {
await mx.setDeviceDetails(device.device_id, { display_name: name });
await refreshDeviceList();
},
[mx, device.device_id, refreshDeviceList]
)
);
const renaming = renameState.status === AsyncStatus.Loading;
useEffect(() => {
if (renameState.status === AsyncStatus.Success) {
onRename();
}
}, [renameState, onRename]);
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
if (renaming) return;
const target = evt.target as HTMLFormElement | undefined;
const nameInput = target?.nameInput as HTMLInputElement | undefined;
if (!nameInput) return;
const sessionName = nameInput.value.trim();
if (!sessionName || sessionName === device.display_name) return;
rename(sessionName);
};
return (
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
<Text size="L400">Session Name</Text>
<Box gap="200">
<Box grow="Yes" direction="Column">
<Input
name="nameInput"
size="300"
variant="Secondary"
radii="300"
defaultValue={device.display_name}
autoFocus
required
readOnly={renaming}
/>
</Box>
<Box shrink="No" gap="200">
<Button
type="submit"
size="300"
variant="Success"
radii="300"
fill="Solid"
disabled={renaming}
before={renaming && <Spinner size="100" variant="Success" fill="Solid" />}
>
<Text size="B300">Save</Text>
</Button>
<Button
type="button"
size="300"
variant="Secondary"
radii="300"
fill="Soft"
onClick={onCancel}
disabled={renaming}
>
<Text size="B300">Cancel</Text>
</Button>
</Box>
</Box>
{renameState.status === AsyncStatus.Error ? (
<Text size="T200" style={{ color: color.Critical.Main }}>
{renameState.error.message}
</Text>
) : (
<Text size="T200">Session names are visible to public.</Text>
)}
</Box>
);
}
type DeviceTileProps = {
device: IMyDevice;
deleted: boolean;
onDeleteToggle: (deviceId: string) => void;
refreshDeviceList: () => Promise<void>;
disabled?: boolean;
};
function DeviceTile({
device,
deleted,
onDeleteToggle,
refreshDeviceList,
disabled,
}: DeviceTileProps) {
const activeTs = device.last_seen_ts;
const [details, setDetails] = useState(false);
const [edit, setEdit] = useState(false);
const handleRename = useCallback(() => {
setEdit(false);
}, []);
return (
<>
<SettingTile
before={
<IconButton
variant={deleted ? 'Critical' : 'Secondary'}
outlined={deleted}
radii="300"
onClick={() => setDetails(!details)}
>
<Icon size="50" src={details ? Icons.ChevronBottom : Icons.ChevronRight} />
</IconButton>
}
after={
!edit && (
<Box shrink="No" alignItems="Center" gap="200">
{deleted ? (
<Chip
variant="Critical"
fill="None"
radii="Pill"
onClick={() => onDeleteToggle?.(device.device_id)}
disabled={disabled}
>
<Text size="B300">Undo</Text>
</Chip>
) : (
<>
<Chip
variant="Secondary"
fill="None"
radii="Pill"
onClick={() => onDeleteToggle?.(device.device_id)}
disabled={disabled}
>
<Icon size="50" src={Icons.Delete} />
</Chip>
<Chip
variant="Secondary"
radii="Pill"
onClick={() => setEdit(true)}
disabled={disabled}
>
<Text size="B300">Edit</Text>
</Chip>
</>
)}
</Box>
)
}
>
<Text size="T300">{device.display_name ?? device.device_id}</Text>
<Box direction="Column">
{typeof activeTs === 'number' && <DeviceActiveTime ts={activeTs} />}
{details && <DeviceDetails device={device} />}
</Box>
</SettingTile>
{edit && (
<DeviceRename
device={device}
onCancel={() => setEdit(false)}
onRename={handleRename}
refreshDeviceList={refreshDeviceList}
/>
)}
</>
);
}
type SessionsProps = {
type DevicesProps = {
requestClose: () => void;
};
export function Sessions({ requestClose }: SessionsProps) {
export function Devices({ requestClose }: DevicesProps) {
const mx = useMatrixClient();
const [devices, refreshDeviceList] = useDeviceList();
const currentDeviceId = mx.getDeviceId();
@ -354,7 +131,7 @@ export function Sessions({ requestClose }: SessionsProps) {
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Sessions
Devices
</Text>
</Box>
<Box shrink="No">
@ -385,11 +162,11 @@ export function Sessions({ requestClose }: SessionsProps) {
<Box grow="Yes" direction="Column">
{deleteError ? (
<Text size="T200">
<b>Failed to logout sessions! Please try again. {deleteError.message}</b>
<b>Failed to logout devices! Please try again. {deleteError.message}</b>
</Text>
) : (
<Text size="T200">
<b>Logout from selected sessions. ({deleted.size} selected)</b>
<b>Logout from selected devices. ({deleted.size} selected)</b>
</Text>
)}
{authData && (
@ -438,6 +215,27 @@ export function Sessions({ requestClose }: SessionsProps) {
</Box>
</Menu>
)}
<Box direction="Column" gap="100">
<Text size="L400">Security</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Device Verification"
description="To verify your identity and grant access to your encrypted messages on another device."
after={
<Button size="300" radii="300">
<Text as="span" size="B300">
Setup
</Text>
</Button>
}
/>
</SequenceCard>
</Box>
{devices === null && <DevicesPlaceholder />}
{currentDevice && (
<Box direction="Column" gap="100">
@ -485,6 +283,7 @@ export function Sessions({ requestClose }: SessionsProps) {
))}
</Box>
)}
<LocalBackup />
</Box>
</PageContent>
</Scroll>

View file

@ -0,0 +1 @@
export * from './Devices';

View file

@ -1,56 +0,0 @@
import React from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll } 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 { LocalBackup } from './LocalBackup';
import { OnlineBackup } from './OnlineBackup';
type EncryptionProps = {
requestClose: () => void;
};
export function Encryption({ requestClose }: EncryptionProps) {
return (
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Encryption
</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 direction="Column" gap="100">
<Text size="L400">Security</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Session Verification"
description="Keep all your sessions secured by prompting an alert to verify new session login."
/>
</SequenceCard>
</Box>
<OnlineBackup />
<LocalBackup />
</Box>
</PageContent>
</Scroll>
</Box>
</Page>
);
}

View file

@ -1,31 +0,0 @@
import React from 'react';
import { Box, Text, Button } from 'folds';
import { SettingTile } from '../../../components/setting-tile';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
export function OnlineBackup() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Online Backup</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Automatic Backup"
description="Continuously save encryption data on server to decrypt messages later."
after={
<Button size="300" variant="Success" radii="300">
<Text as="span" size="B300">
Restore
</Text>
</Button>
}
/>
</SequenceCard>
</Box>
);
}

View file

@ -1 +0,0 @@
export * from './Encryption';

View file

@ -1 +0,0 @@
export * from './Sessions';