From a50b6efd46e1f86941fa867f7dda6a784923ec84 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 26 Jan 2025 20:58:24 +0530 Subject: [PATCH] merge encryption tab into sessions and rename it to devices --- src/app/features/settings/Settings.tsx | 22 +- .../features/settings/devices/DeviceTile.tsx | 223 +++++++++++++++ .../Sessions.tsx => devices/Devices.tsx} | 265 +++--------------- .../{encryption => devices}/LocalBackup.tsx | 0 src/app/features/settings/devices/index.ts | 1 + .../settings/encryption/Encryption.tsx | 56 ---- .../settings/encryption/OnlineBackup.tsx | 31 -- src/app/features/settings/encryption/index.ts | 1 - src/app/features/settings/sessions/index.ts | 1 - 9 files changed, 262 insertions(+), 338 deletions(-) create mode 100644 src/app/features/settings/devices/DeviceTile.tsx rename src/app/features/settings/{sessions/Sessions.tsx => devices/Devices.tsx} (56%) rename src/app/features/settings/{encryption => devices}/LocalBackup.tsx (100%) create mode 100644 src/app/features/settings/devices/index.ts delete mode 100644 src/app/features/settings/encryption/Encryption.tsx delete mode 100644 src/app/features/settings/encryption/OnlineBackup.tsx delete mode 100644 src/app/features/settings/encryption/index.ts delete mode 100644 src/app/features/settings/sessions/index.ts diff --git a/src/app/features/settings/Settings.tsx b/src/app/features/settings/Settings.tsx index 5dd954c6..11ac2fa2 100644 --- a/src/app/features/settings/Settings.tsx +++ b/src/app/features/settings/Settings.tsx @@ -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} /> diff --git a/src/app/features/settings/devices/DeviceTile.tsx b/src/app/features/settings/devices/DeviceTile.tsx new file mode 100644 index 00000000..314d3545 --- /dev/null +++ b/src/app/features/settings/devices/DeviceTile.tsx @@ -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} + /> + )} + </> + ); +} diff --git a/src/app/features/settings/sessions/Sessions.tsx b/src/app/features/settings/devices/Devices.tsx similarity index 56% rename from src/app/features/settings/sessions/Sessions.tsx rename to src/app/features/settings/devices/Devices.tsx index dcc69091..fb1ee4e2 100644 --- a/src/app/features/settings/sessions/Sessions.tsx +++ b/src/app/features/settings/devices/Devices.tsx @@ -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> diff --git a/src/app/features/settings/encryption/LocalBackup.tsx b/src/app/features/settings/devices/LocalBackup.tsx similarity index 100% rename from src/app/features/settings/encryption/LocalBackup.tsx rename to src/app/features/settings/devices/LocalBackup.tsx diff --git a/src/app/features/settings/devices/index.ts b/src/app/features/settings/devices/index.ts new file mode 100644 index 00000000..35b5f867 --- /dev/null +++ b/src/app/features/settings/devices/index.ts @@ -0,0 +1 @@ +export * from './Devices'; diff --git a/src/app/features/settings/encryption/Encryption.tsx b/src/app/features/settings/encryption/Encryption.tsx deleted file mode 100644 index 1125cc69..00000000 --- a/src/app/features/settings/encryption/Encryption.tsx +++ /dev/null @@ -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> - ); -} diff --git a/src/app/features/settings/encryption/OnlineBackup.tsx b/src/app/features/settings/encryption/OnlineBackup.tsx deleted file mode 100644 index 74c1a9f6..00000000 --- a/src/app/features/settings/encryption/OnlineBackup.tsx +++ /dev/null @@ -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> - ); -} diff --git a/src/app/features/settings/encryption/index.ts b/src/app/features/settings/encryption/index.ts deleted file mode 100644 index 8e1560ef..00000000 --- a/src/app/features/settings/encryption/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Encryption'; diff --git a/src/app/features/settings/sessions/index.ts b/src/app/features/settings/sessions/index.ts deleted file mode 100644 index 24947d86..00000000 --- a/src/app/features/settings/sessions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Sessions';