import React, { useState, useEffect } from 'react'; import './DeviceManage.scss'; import dateFormat from 'dateformat'; import initMatrix from '../../../client/initMatrix'; import { isCrossVerified } from '../../../util/matrixUtil'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; 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 { useStore } from '../../hooks/useStore'; import { useDeviceList } from '../../hooks/useDeviceList'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; 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); 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 = window.prompt('Edit session name', 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) => { if (window.confirm(`You are about to logout "${device.display_name}" session.`)) { addToProcessing(device); await authRequest(`Logout "${device.display_name}"`, async (auth) => { await mx.deleteDevice(device.device_id, auth); }); if (!mountStore.getItem()) return; removeFromProcessing(device); } }; 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; return ( {displayName} {` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`} )} options={ processing.includes(deviceId) ? : ( <> handleRename(device)} src={PencilIC} tooltip="Rename" /> handleRemove(device)} src={BinIC} tooltip="Remove session" /> ) } content={( Last activity {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')} {lastIP ? ` at ${lastIP}` : ''} )} /> ); }; 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 {!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, true))}
)}
Verified sessions { verified.length > 0 ? verified.map((device, index) => { if (truncated && index >= TRUNCATED_COUNT) return null; return renderDevice(device, true); }) : No verified session } { verified.length > TRUNCATED_COUNT && ( )} { deviceList.length > 0 && ( Session names are visible to everyone, so do not put any private info here. )}
); } export default DeviceManage;