import React, { useState, useEffect } from 'react'; import './DeviceManage.scss'; import dateFormat from 'dateformat'; import initMatrix from '../../../client/initMatrix'; import { isCrossVerified } from '../../../util/matrixUtil'; import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; import Input from '../../atoms/input/Input'; import { MenuHeader } from '../../atoms/context-menu/ContextMenu'; import InfoCard from '../../atoms/card/InfoCard'; import Spinner from '../../atoms/spinner/Spinner'; import SettingTile from '../../molecules/setting-tile/SettingTile'; import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; import BinIC from '../../../../public/res/ic/outlined/bin.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg'; import { authRequest } from './AuthRequest'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { useStore } from '../../hooks/useStore'; import { useDeviceList } from '../../hooks/useDeviceList'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; import { accessSecretStorage } from './SecretStorageAccess'; 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); }; return (
); }; openReusableDialog( Edit session name, (requestClose) => renderContent((name) => { isCompleted = true; resolve(name); requestClose(); }), () => { if (!isCompleted) resolve(null); }, ); }); function DeviceManage() { const TRUNCATED_COUNT = 4; const mx = initMatrix.matrixClient; const isCSEnabled = useCrossSigningStatus(); const deviceList = useDeviceList(); const [processing, setProcessing] = useState([]); const [truncated, setTruncated] = useState(true); const mountStore = useStore(); mountStore.setItem(true); const isMeVerified = isCrossVerified(mx.deviceId); useEffect(() => { setProcessing([]); }, [deviceList]); const addToProcessing = (device) => { const old = [...processing]; old.push(device.device_id); setProcessing(old); }; const removeFromProcessing = () => { setProcessing([]); }; if (deviceList === null) { return (
Loading devices...
); } const handleRename = async (device) => { const newName = await promptDeviceName(device.display_name); if (newName === null || newName.trim() === '') return; if (newName.trim() === device.display_name) return; addToProcessing(device); try { await mx.setDeviceDetails(device.device_id, { display_name: newName, }); } catch { if (!mountStore.getItem()) return; removeFromProcessing(device); } }; const handleRemove = async (device) => { const isConfirmed = await confirmDialog( `Logout ${device.display_name}`, `You are about to logout "${device.display_name}" session.`, 'Logout', 'danger', ); if (!isConfirmed) return; addToProcessing(device); await authRequest(`Logout "${device.display_name}"`, async (auth) => { await mx.deleteDevice(device.device_id, auth); }); if (!mountStore.getItem()) return; removeFromProcessing(device); }; const verifyWithKey = async (device) => { const keyData = await accessSecretStorage('Session verification'); if (!keyData) return; addToProcessing(device); await mx.checkOwnCrossSigningTrust(); }; const verifyWithEmojis = async (deviceId) => { const req = await mx.requestVerification(mx.getUserId(), [deviceId]); openEmojiVerification(req, { userId: mx.getUserId(), deviceId }); }; const verify = (deviceId, isCurrentDevice) => { if (isCurrentDevice) { verifyWithKey(deviceId); return; } verifyWithEmojis(deviceId); }; const renderDevice = (device, isVerified) => { const deviceId = device.device_id; const displayName = device.display_name; const lastIP = device.last_seen_ip; const lastTS = device.last_seen_ts; const isCurrentDevice = mx.deviceId === deviceId; const canVerify = isVerified === false && (isMeVerified || isCurrentDevice); return ( {displayName} {`${displayName ? ' — ' : ''}${deviceId}`} {isCurrentDevice && Current} )} options={ processing.includes(deviceId) ? : ( <> {(isCSEnabled && canVerify) && } handleRename(device)} src={PencilIC} tooltip="Rename" /> handleRemove(device)} src={BinIC} tooltip="Remove session" /> ) } content={( <> {lastTS && ( Last activity {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')} {lastIP ? ` at ${lastIP}` : ''} )} {isCurrentDevice && ( {`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`} )} )} /> ); }; const unverified = []; const verified = []; const noEncryption = []; deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => { const isVerified = isCrossVerified(device.device_id); if (isVerified === true) { verified.push(device); } else if (isVerified === false) { unverified.push(device); } else { noEncryption.push(device); } }); return (
Unverified sessions {!isMeVerified && (
)} {isMeVerified && unverified.length > 0 && (
)} {!isCSEnabled && (
)} { unverified.length > 0 ? unverified.map((device) => renderDevice(device, false)) : No unverified sessions }
{noEncryption.length > 0 && (
Sessions without encryption support {noEncryption.map((device) => renderDevice(device, null))}
)}
Verified sessions { verified.length > 0 ? verified.map((device, index) => { if (truncated && index >= TRUNCATED_COUNT) return null; return renderDevice(device, true); }) : No verified sessions } { verified.length > TRUNCATED_COUNT && ( )} { deviceList.length > 0 && ( Session names are visible to everyone, so do not put any private info here. )}
); } export default DeviceManage;