mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-03-13 06:30:01 +01:00
user session settings - WIP
This commit is contained in:
parent
fff4a9cbe9
commit
f74f104406
6 changed files with 499 additions and 97 deletions
|
@ -11,6 +11,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { UserAvatar } from '../../components/user-avatar';
|
import { UserAvatar } from '../../components/user-avatar';
|
||||||
import { nameInitials } from '../../utils/common';
|
import { nameInitials } from '../../utils/common';
|
||||||
import { Notifications } from './notifications';
|
import { Notifications } from './notifications';
|
||||||
|
import { Sessions } from './sessions';
|
||||||
import { EmojisStickers } from './emojis-stickers';
|
import { EmojisStickers } from './emojis-stickers';
|
||||||
import { DeveloperTools } from './developer-tools';
|
import { DeveloperTools } from './developer-tools';
|
||||||
import { About } from './about';
|
import { About } from './about';
|
||||||
|
@ -19,7 +20,7 @@ enum SettingsPages {
|
||||||
GeneralPage,
|
GeneralPage,
|
||||||
AccountPage,
|
AccountPage,
|
||||||
NotificationPage,
|
NotificationPage,
|
||||||
SessionPage,
|
SessionsPage,
|
||||||
EncryptionPage,
|
EncryptionPage,
|
||||||
EmojisStickersPage,
|
EmojisStickersPage,
|
||||||
DeveloperToolsPage,
|
DeveloperToolsPage,
|
||||||
|
@ -51,7 +52,7 @@ const useSettingsMenuItems = (): SettingsMenuItem[] =>
|
||||||
icon: Icons.Bell,
|
icon: Icons.Bell,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
page: SettingsPages.SessionPage,
|
page: SettingsPages.SessionsPage,
|
||||||
name: 'Sessions',
|
name: 'Sessions',
|
||||||
icon: Icons.Category,
|
icon: Icons.Category,
|
||||||
},
|
},
|
||||||
|
@ -171,6 +172,9 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
||||||
{activePage === SettingsPages.NotificationPage && (
|
{activePage === SettingsPages.NotificationPage && (
|
||||||
<Notifications requestClose={handlePageRequestClose} />
|
<Notifications requestClose={handlePageRequestClose} />
|
||||||
)}
|
)}
|
||||||
|
{activePage === SettingsPages.SessionsPage && (
|
||||||
|
<Sessions requestClose={handlePageRequestClose} />
|
||||||
|
)}
|
||||||
{activePage === SettingsPages.EmojisStickersPage && (
|
{activePage === SettingsPages.EmojisStickersPage && (
|
||||||
<EmojisStickers requestClose={handlePageRequestClose} />
|
<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 */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { CryptoEvent, IMyDevice } from 'matrix-js-sdk';
|
import { IMyDevice } from 'matrix-js-sdk';
|
||||||
import { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||||
import { useMatrixClient } from './useMatrixClient';
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
import { useAlive } from './useAlive';
|
||||||
|
|
||||||
export function useDeviceList() {
|
export function useDeviceList(): [null | IMyDevice[], () => Promise<void>] {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const [deviceList, setDeviceList] = useState<IMyDevice[] | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
refreshDeviceList();
|
||||||
|
|
||||||
const updateDevices = () =>
|
|
||||||
mx.getDevices().then((data) => {
|
|
||||||
if (!isMounted) return;
|
|
||||||
setDeviceList(data.devices || []);
|
|
||||||
});
|
|
||||||
updateDevices();
|
|
||||||
|
|
||||||
const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => {
|
const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => {
|
||||||
const userId = mx.getUserId();
|
const userId = mx.getUserId();
|
||||||
if (userId && users.includes(userId)) {
|
if (userId && users.includes(userId)) {
|
||||||
updateDevices();
|
refreshDeviceList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||||
return () => {
|
return () => {
|
||||||
mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||||
isMounted = false;
|
|
||||||
};
|
};
|
||||||
}, [mx]);
|
}, [mx, refreshDeviceList]);
|
||||||
return deviceList;
|
|
||||||
|
return [deviceList, refreshDeviceList];
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,45 +27,51 @@ import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
|
||||||
import { accessSecretStorage } from './SecretStorageAccess';
|
import { accessSecretStorage } from './SecretStorageAccess';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
|
const promptDeviceName = async (deviceName) =>
|
||||||
let isCompleted = false;
|
new Promise((resolve) => {
|
||||||
|
let isCompleted = false;
|
||||||
|
|
||||||
const renderContent = (onComplete) => {
|
const renderContent = (onComplete) => {
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const name = e.target.session.value;
|
const name = e.target.session.value;
|
||||||
if (typeof name !== 'string') onComplete(null);
|
if (typeof name !== 'string') onComplete(null);
|
||||||
onComplete(name);
|
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(
|
openReusableDialog(
|
||||||
<Text variant="s1" weight="medium">Edit session name</Text>,
|
<Text variant="s1" weight="medium">
|
||||||
(requestClose) => renderContent((name) => {
|
Edit session name
|
||||||
isCompleted = true;
|
</Text>,
|
||||||
resolve(name);
|
(requestClose) =>
|
||||||
requestClose();
|
renderContent((name) => {
|
||||||
}),
|
isCompleted = true;
|
||||||
() => {
|
resolve(name);
|
||||||
if (!isCompleted) resolve(null);
|
requestClose();
|
||||||
},
|
}),
|
||||||
);
|
() => {
|
||||||
});
|
if (!isCompleted) resolve(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function DeviceManage() {
|
function DeviceManage() {
|
||||||
const TRUNCATED_COUNT = 4;
|
const TRUNCATED_COUNT = 4;
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const isCSEnabled = useCrossSigningStatus();
|
const isCSEnabled = useCrossSigningStatus();
|
||||||
const deviceList = useDeviceList();
|
const [deviceList] = useDeviceList();
|
||||||
const [processing, setProcessing] = useState([]);
|
const [processing, setProcessing] = useState([]);
|
||||||
const [truncated, setTruncated] = useState(true);
|
const [truncated, setTruncated] = useState(true);
|
||||||
const mountStore = useStore();
|
const mountStore = useStore();
|
||||||
|
@ -117,7 +123,7 @@ function DeviceManage() {
|
||||||
`Logout ${device.display_name}`,
|
`Logout ${device.display_name}`,
|
||||||
`You are about to logout "${device.display_name}" session.`,
|
`You are about to logout "${device.display_name}" session.`,
|
||||||
'Logout',
|
'Logout',
|
||||||
'danger',
|
'danger'
|
||||||
);
|
);
|
||||||
if (!isConfirmed) return;
|
if (!isConfirmed) return;
|
||||||
addToProcessing(device);
|
addToProcessing(device);
|
||||||
|
@ -160,25 +166,43 @@ function DeviceManage() {
|
||||||
return (
|
return (
|
||||||
<SettingTile
|
<SettingTile
|
||||||
key={deviceId}
|
key={deviceId}
|
||||||
title={(
|
title={
|
||||||
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
|
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
|
||||||
{displayName}
|
{displayName}
|
||||||
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
|
<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>
|
</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 && (
|
{lastTS && (
|
||||||
<Text variant="b3">
|
<Text variant="b3">
|
||||||
|
@ -191,11 +215,14 @@ function DeviceManage() {
|
||||||
)}
|
)}
|
||||||
{isCurrentDevice && (
|
{isCurrentDevice && (
|
||||||
<Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
|
<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>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -203,16 +230,18 @@ function DeviceManage() {
|
||||||
const unverified = [];
|
const unverified = [];
|
||||||
const verified = [];
|
const verified = [];
|
||||||
const noEncryption = [];
|
const noEncryption = [];
|
||||||
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
|
deviceList
|
||||||
const isVerified = isCrossVerified(mx, device.device_id);
|
.sort((a, b) => b.last_seen_ts - a.last_seen_ts)
|
||||||
if (isVerified === true) {
|
.forEach((device) => {
|
||||||
verified.push(device);
|
const isVerified = isCrossVerified(mx, device.device_id);
|
||||||
} else if (isVerified === false) {
|
if (isVerified === true) {
|
||||||
unverified.push(device);
|
verified.push(device);
|
||||||
} else {
|
} else if (isVerified === false) {
|
||||||
noEncryption.push(device);
|
unverified.push(device);
|
||||||
}
|
} else {
|
||||||
});
|
noEncryption.push(device);
|
||||||
|
}
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className="device-manage">
|
<div className="device-manage">
|
||||||
<div>
|
<div>
|
||||||
|
@ -247,35 +276,37 @@ function DeviceManage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{
|
{unverified.length > 0 ? (
|
||||||
unverified.length > 0
|
unverified.map((device) => renderDevice(device, false))
|
||||||
? unverified.map((device) => renderDevice(device, false))
|
) : (
|
||||||
: <Text className="device-manage__info">No unverified sessions</Text>
|
<Text className="device-manage__info">No unverified sessions</Text>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{noEncryption.length > 0 && (
|
{noEncryption.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<MenuHeader>Sessions without encryption support</MenuHeader>
|
<MenuHeader>Sessions without encryption support</MenuHeader>
|
||||||
{noEncryption.map((device) => renderDevice(device, null))}
|
{noEncryption.map((device) => renderDevice(device, null))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<MenuHeader>Verified sessions</MenuHeader>
|
<MenuHeader>Verified sessions</MenuHeader>
|
||||||
{
|
{verified.length > 0 ? (
|
||||||
verified.length > 0
|
verified.map((device, index) => {
|
||||||
? verified.map((device, index) => {
|
if (truncated && index >= TRUNCATED_COUNT) return null;
|
||||||
if (truncated && index >= TRUNCATED_COUNT) return null;
|
return renderDevice(device, true);
|
||||||
return renderDevice(device, true);
|
})
|
||||||
})
|
) : (
|
||||||
: <Text className="device-manage__info">No verified sessions</Text>
|
<Text className="device-manage__info">No verified sessions</Text>
|
||||||
}
|
)}
|
||||||
{ verified.length > TRUNCATED_COUNT && (
|
{verified.length > TRUNCATED_COUNT && (
|
||||||
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
|
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
|
||||||
{truncated ? `View ${verified.length - 4} more` : 'View less'}
|
{truncated ? `View ${verified.length - 4} more` : 'View less'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{ deviceList.length > 0 && (
|
{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>
|
<Text className="device-manage__info" variant="b3">
|
||||||
|
Session names are visible to everyone, so do not put any private info here.
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,7 @@ import * as css from './UnverifiedTab.css';
|
||||||
|
|
||||||
export function UnverifiedTab() {
|
export function UnverifiedTab() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const deviceList = useDeviceList();
|
const [deviceList] = useDeviceList();
|
||||||
const unverified = deviceList?.filter(
|
const unverified = deviceList?.filter(
|
||||||
(device) => isCrossVerified(mx, device.device_id) === false
|
(device) => isCrossVerified(mx, device.device_id) === false
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue