mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-13 17:10:06 +00:00
user session settings - WIP
This commit is contained in:
parent
fff4a9cbe9
commit
f74f104406
src/app
features/settings
hooks
organisms/settings
pages/client/sidebar
|
@ -11,6 +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 { EmojisStickers } from './emojis-stickers';
|
||||
import { DeveloperTools } from './developer-tools';
|
||||
import { About } from './about';
|
||||
|
@ -19,7 +20,7 @@ enum SettingsPages {
|
|||
GeneralPage,
|
||||
AccountPage,
|
||||
NotificationPage,
|
||||
SessionPage,
|
||||
SessionsPage,
|
||||
EncryptionPage,
|
||||
EmojisStickersPage,
|
||||
DeveloperToolsPage,
|
||||
|
@ -51,7 +52,7 @@ const useSettingsMenuItems = (): SettingsMenuItem[] =>
|
|||
icon: Icons.Bell,
|
||||
},
|
||||
{
|
||||
page: SettingsPages.SessionPage,
|
||||
page: SettingsPages.SessionsPage,
|
||||
name: 'Sessions',
|
||||
icon: Icons.Category,
|
||||
},
|
||||
|
@ -171,6 +172,9 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
|||
{activePage === SettingsPages.NotificationPage && (
|
||||
<Notifications requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SettingsPages.SessionsPage && (
|
||||
<Sessions requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
{activePage === SettingsPages.EmojisStickersPage && (
|
||||
<EmojisStickers requestClose={handlePageRequestClose} />
|
||||
)}
|
||||
|
|
365
src/app/features/settings/sessions/Sessions.tsx
Normal file
365
src/app/features/settings/sessions/Sessions.tsx
Normal file
|
@ -0,0 +1,365 @@
|
|||
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
IconButton,
|
||||
Icon,
|
||||
Icons,
|
||||
Scroll,
|
||||
Chip,
|
||||
Input,
|
||||
Button,
|
||||
color,
|
||||
Spinner,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { IMyDevice, 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 { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
|
||||
function DevicesPlaceholder() {
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
style={{ height: toRem(64) }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
/>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
style={{ height: toRem(64) }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
/>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
style={{ height: toRem(64) }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
/>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
style={{ height: toRem(64) }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
/>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
style={{ height: toRem(64) }}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type DeviceTileProps = {
|
||||
device: IMyDevice;
|
||||
deleted: boolean;
|
||||
onDeleteToggle: (deviceId: string) => void;
|
||||
refreshDeviceList: () => Promise<void>;
|
||||
};
|
||||
function DeviceTile({ device, deleted, onDeleteToggle, refreshDeviceList }: 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.ChevronTop : Icons.ChevronBottom} />
|
||||
</IconButton>
|
||||
}
|
||||
after={
|
||||
!edit && (
|
||||
<Box shrink="No" alignItems="Center" gap="200">
|
||||
{deleted ? (
|
||||
<Chip
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="Pill"
|
||||
onClick={() => onDeleteToggle?.(device.device_id)}
|
||||
>
|
||||
<Text size="B300">Undo</Text>
|
||||
</Chip>
|
||||
) : (
|
||||
<>
|
||||
<Chip
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
radii="Pill"
|
||||
onClick={() => onDeleteToggle?.(device.device_id)}
|
||||
>
|
||||
<Icon size="50" src={Icons.Delete} />
|
||||
</Chip>
|
||||
<Chip variant="Secondary" radii="Pill" onClick={() => setEdit(true)}>
|
||||
<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 = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function Sessions({ requestClose }: SessionsProps) {
|
||||
const mx = useMatrixClient();
|
||||
const [devices, refreshDeviceList] = useDeviceList();
|
||||
const currentDeviceId = mx.getDeviceId();
|
||||
const currentDevice = currentDeviceId
|
||||
? devices?.find((device) => device.device_id === currentDeviceId)
|
||||
: undefined;
|
||||
const otherDevices = devices?.filter((device) => device.device_id !== currentDeviceId);
|
||||
|
||||
const [deleted, setDeleted] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleToggleDelete = useCallback((deviceId: string) => {
|
||||
setDeleted((deviceIds) => {
|
||||
const newIds = new Set(deviceIds);
|
||||
if (newIds.has(deviceId)) {
|
||||
newIds.delete(deviceId);
|
||||
} else {
|
||||
newIds.add(deviceId);
|
||||
}
|
||||
return newIds;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
Sessions
|
||||
</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">
|
||||
{devices === null && <DevicesPlaceholder />}
|
||||
{currentDevice && (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Current</Text>
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant={deleted.has(currentDevice.device_id) ? 'Critical' : 'SurfaceVariant'}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<DeviceTile
|
||||
device={currentDevice}
|
||||
deleted={deleted.has(currentDevice.device_id)}
|
||||
onDeleteToggle={handleToggleDelete}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
)}
|
||||
{otherDevices && otherDevices.length > 0 && (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Others</Text>
|
||||
{otherDevices
|
||||
.sort((d1, d2) => {
|
||||
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
|
||||
return d1.last_seen_ts < d2.last_seen_ts ? 1 : -1;
|
||||
})
|
||||
.map((device) => (
|
||||
<SequenceCard
|
||||
key={device.device_id}
|
||||
className={SequenceCardStyle}
|
||||
variant={deleted.has(device.device_id) ? 'Critical' : 'SurfaceVariant'}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<DeviceTile
|
||||
device={device}
|
||||
deleted={deleted.has(device.device_id)}
|
||||
onDeleteToggle={handleToggleDelete}
|
||||
refreshDeviceList={refreshDeviceList}
|
||||
/>
|
||||
</SequenceCard>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
1
src/app/features/settings/sessions/index.ts
Normal file
1
src/app/features/settings/sessions/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Sessions';
|
|
@ -1,35 +1,36 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CryptoEvent, IMyDevice } from 'matrix-js-sdk';
|
||||
import { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { IMyDevice } from 'matrix-js-sdk';
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useAlive } from './useAlive';
|
||||
|
||||
export function useDeviceList() {
|
||||
export function useDeviceList(): [null | IMyDevice[], () => Promise<void>] {
|
||||
const mx = useMatrixClient();
|
||||
const [deviceList, setDeviceList] = useState<IMyDevice[] | null>(null);
|
||||
const alive = useAlive();
|
||||
|
||||
const refreshDeviceList = useCallback(async () => {
|
||||
const data = await mx.getDevices();
|
||||
if (!alive()) return;
|
||||
setDeviceList(data.devices || []);
|
||||
}, [mx, alive]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const updateDevices = () =>
|
||||
mx.getDevices().then((data) => {
|
||||
if (!isMounted) return;
|
||||
setDeviceList(data.devices || []);
|
||||
});
|
||||
updateDevices();
|
||||
refreshDeviceList();
|
||||
|
||||
const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => {
|
||||
const userId = mx.getUserId();
|
||||
if (userId && users.includes(userId)) {
|
||||
updateDevices();
|
||||
refreshDeviceList();
|
||||
}
|
||||
};
|
||||
|
||||
mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||
isMounted = false;
|
||||
};
|
||||
}, [mx]);
|
||||
return deviceList;
|
||||
}, [mx, refreshDeviceList]);
|
||||
|
||||
return [deviceList, refreshDeviceList];
|
||||
}
|
||||
|
|
|
@ -27,45 +27,51 @@ import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
|
|||
import { accessSecretStorage } from './SecretStorageAccess';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
const promptDeviceName = async (deviceName) =>
|
||||
new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
|
||||
const renderContent = (onComplete) => {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const name = e.target.session.value;
|
||||
if (typeof name !== 'string') onComplete(null);
|
||||
onComplete(name);
|
||||
const renderContent = (onComplete) => {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const name = e.target.session.value;
|
||||
if (typeof name !== 'string') onComplete(null);
|
||||
onComplete(name);
|
||||
};
|
||||
return (
|
||||
<form className="device-manage__rename" onSubmit={handleSubmit}>
|
||||
<Input value={deviceName} label="Session name" name="session" />
|
||||
<div className="device-manage__rename-btn">
|
||||
<Button variant="primary" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={() => onComplete(null)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<form className="device-manage__rename" onSubmit={handleSubmit}>
|
||||
<Input value={deviceName} label="Session name" name="session" />
|
||||
<div className="device-manage__rename-btn">
|
||||
<Button variant="primary" type="submit">Save</Button>
|
||||
<Button onClick={() => onComplete(null)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Edit session name</Text>,
|
||||
(requestClose) => renderContent((name) => {
|
||||
isCompleted = true;
|
||||
resolve(name);
|
||||
requestClose();
|
||||
}),
|
||||
() => {
|
||||
if (!isCompleted) resolve(null);
|
||||
},
|
||||
);
|
||||
});
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">
|
||||
Edit session name
|
||||
</Text>,
|
||||
(requestClose) =>
|
||||
renderContent((name) => {
|
||||
isCompleted = true;
|
||||
resolve(name);
|
||||
requestClose();
|
||||
}),
|
||||
() => {
|
||||
if (!isCompleted) resolve(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function DeviceManage() {
|
||||
const TRUNCATED_COUNT = 4;
|
||||
const mx = useMatrixClient();
|
||||
const isCSEnabled = useCrossSigningStatus();
|
||||
const deviceList = useDeviceList();
|
||||
const [deviceList] = useDeviceList();
|
||||
const [processing, setProcessing] = useState([]);
|
||||
const [truncated, setTruncated] = useState(true);
|
||||
const mountStore = useStore();
|
||||
|
@ -117,7 +123,7 @@ function DeviceManage() {
|
|||
`Logout ${device.display_name}`,
|
||||
`You are about to logout "${device.display_name}" session.`,
|
||||
'Logout',
|
||||
'danger',
|
||||
'danger'
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
addToProcessing(device);
|
||||
|
@ -160,25 +166,43 @@ function DeviceManage() {
|
|||
return (
|
||||
<SettingTile
|
||||
key={deviceId}
|
||||
title={(
|
||||
title={
|
||||
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
|
||||
{displayName}
|
||||
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
|
||||
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
|
||||
{isCurrentDevice && (
|
||||
<Text span className="device-manage__current-label" variant="b3">
|
||||
Current
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
options={
|
||||
processing.includes(deviceId)
|
||||
? <Spinner size="small" />
|
||||
: (
|
||||
<>
|
||||
{(isCSEnabled && canVerify) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>}
|
||||
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
|
||||
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
content={(
|
||||
options={
|
||||
processing.includes(deviceId) ? (
|
||||
<Spinner size="small" />
|
||||
) : (
|
||||
<>
|
||||
{isCSEnabled && canVerify && (
|
||||
<Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">
|
||||
Verify
|
||||
</Button>
|
||||
)}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRename(device)}
|
||||
src={PencilIC}
|
||||
tooltip="Rename"
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRemove(device)}
|
||||
src={BinIC}
|
||||
tooltip="Remove session"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
content={
|
||||
<>
|
||||
{lastTS && (
|
||||
<Text variant="b3">
|
||||
|
@ -191,11 +215,14 @@ function DeviceManage() {
|
|||
)}
|
||||
{isCurrentDevice && (
|
||||
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
|
||||
{`Session Key: ${mx.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
|
||||
{`Session Key: ${mx
|
||||
.getDeviceEd25519Key()
|
||||
.match(/.{1,4}/g)
|
||||
.join(' ')}`}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -203,16 +230,18 @@ function DeviceManage() {
|
|||
const unverified = [];
|
||||
const verified = [];
|
||||
const noEncryption = [];
|
||||
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
|
||||
const isVerified = isCrossVerified(mx, device.device_id);
|
||||
if (isVerified === true) {
|
||||
verified.push(device);
|
||||
} else if (isVerified === false) {
|
||||
unverified.push(device);
|
||||
} else {
|
||||
noEncryption.push(device);
|
||||
}
|
||||
});
|
||||
deviceList
|
||||
.sort((a, b) => b.last_seen_ts - a.last_seen_ts)
|
||||
.forEach((device) => {
|
||||
const isVerified = isCrossVerified(mx, device.device_id);
|
||||
if (isVerified === true) {
|
||||
verified.push(device);
|
||||
} else if (isVerified === false) {
|
||||
unverified.push(device);
|
||||
} else {
|
||||
noEncryption.push(device);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="device-manage">
|
||||
<div>
|
||||
|
@ -247,35 +276,37 @@ function DeviceManage() {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
unverified.length > 0
|
||||
? unverified.map((device) => renderDevice(device, false))
|
||||
: <Text className="device-manage__info">No unverified sessions</Text>
|
||||
}
|
||||
{unverified.length > 0 ? (
|
||||
unverified.map((device) => renderDevice(device, false))
|
||||
) : (
|
||||
<Text className="device-manage__info">No unverified sessions</Text>
|
||||
)}
|
||||
</div>
|
||||
{noEncryption.length > 0 && (
|
||||
<div>
|
||||
<MenuHeader>Sessions without encryption support</MenuHeader>
|
||||
{noEncryption.map((device) => renderDevice(device, null))}
|
||||
</div>
|
||||
<div>
|
||||
<MenuHeader>Sessions without encryption support</MenuHeader>
|
||||
{noEncryption.map((device) => renderDevice(device, null))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<MenuHeader>Verified sessions</MenuHeader>
|
||||
{
|
||||
verified.length > 0
|
||||
? verified.map((device, index) => {
|
||||
if (truncated && index >= TRUNCATED_COUNT) return null;
|
||||
return renderDevice(device, true);
|
||||
})
|
||||
: <Text className="device-manage__info">No verified sessions</Text>
|
||||
}
|
||||
{ verified.length > TRUNCATED_COUNT && (
|
||||
{verified.length > 0 ? (
|
||||
verified.map((device, index) => {
|
||||
if (truncated && index >= TRUNCATED_COUNT) return null;
|
||||
return renderDevice(device, true);
|
||||
})
|
||||
) : (
|
||||
<Text className="device-manage__info">No verified sessions</Text>
|
||||
)}
|
||||
{verified.length > TRUNCATED_COUNT && (
|
||||
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
|
||||
{truncated ? `View ${verified.length - 4} more` : 'View less'}
|
||||
</Button>
|
||||
)}
|
||||
{ deviceList.length > 0 && (
|
||||
<Text className="device-manage__info" variant="b3">Session names are visible to everyone, so do not put any private info here.</Text>
|
||||
{deviceList.length > 0 && (
|
||||
<Text className="device-manage__info" variant="b3">
|
||||
Session names are visible to everyone, so do not put any private info here.
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ import * as css from './UnverifiedTab.css';
|
|||
|
||||
export function UnverifiedTab() {
|
||||
const mx = useMatrixClient();
|
||||
const deviceList = useDeviceList();
|
||||
const [deviceList] = useDeviceList();
|
||||
const unverified = deviceList?.filter(
|
||||
(device) => isCrossVerified(mx, device.device_id) === false
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue