--- /dev/null
+/* eslint-disable react/prop-types */
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import './EmojiVerification.scss';
+import { twemojify } from '../../../util/twemojify';
+
+import initMatrix from '../../../client/initMatrix';
+import cons from '../../../client/state/cons';
+import navigation from '../../../client/state/navigation';
+
+import Text from '../../atoms/text/Text';
+import IconButton from '../../atoms/button/IconButton';
+import Button from '../../atoms/button/Button';
+import Spinner from '../../atoms/spinner/Spinner';
+import Dialog from '../../molecules/dialog/Dialog';
+
+import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+import { useStore } from '../../hooks/useStore';
+
+function EmojiVerificationContent({ request, requestClose }) {
+ const [sas, setSas] = useState(null);
+ const [process, setProcess] = useState(false);
+ const mountStore = useStore();
+ mountStore.setItem(true);
+
+ const handleChange = () => {
+ if (request.done || request.cancelled) requestClose();
+ };
+
+ useEffect(() => {
+ mountStore.setItem(true);
+ if (request === null) return null;
+ const req = request;
+ req.on('change', handleChange);
+ return () => req.off('change', handleChange);
+ }, [request]);
+
+ const acceptRequest = async () => {
+ setProcess(true);
+ await request.accept();
+
+ const verifier = request.beginKeyVerification('m.sas.v1');
+ verifier.on('show_sas', (data) => {
+ if (!mountStore.getItem()) return;
+ setSas(data);
+ setProcess(false);
+ });
+ await verifier.verify();
+ };
+
+ const sasMismatch = () => {
+ sas.mismatch();
+ setProcess(true);
+ };
+
+ const sasConfirm = () => {
+ sas.confirm();
+ setProcess(true);
+ };
+
+ const renderWait = () => (
+ <>
+ <Spinner size="small" />
+ <Text>Waiting for response from other device...</Text>
+ </>
+ );
+
+ if (sas !== null) {
+ return (
+ <div className="emoji-verification__content">
+ <Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
+ <div className="emoji-verification__emojis">
+ {sas.sas.emoji.map((emoji) => (
+ <div className="emoji-verification__emoji-block" key={emoji[1]}>
+ <Text variant="h1">{twemojify(emoji[0])}</Text>
+ <Text>{emoji[1]}</Text>
+ </div>
+ ))}
+ </div>
+ <div className="emoji-verification__buttons">
+ {process ? renderWait() : (
+ <>
+ <Button variant="primary" onClick={sasConfirm}>They match</Button>
+ <Button onClick={sasMismatch}>{'They don\'t match'}</Button>
+ </>
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="emoji-verification__content">
+ <Text>Click accept to start the verification process</Text>
+ <div className="emoji-verification__buttons">
+ {
+ process
+ ? renderWait()
+ : <Button variant="primary" onClick={acceptRequest}>Accept</Button>
+ }
+ </div>
+ </div>
+ );
+}
+EmojiVerificationContent.propTypes = {
+ request: PropTypes.shape({}).isRequired,
+ requestClose: PropTypes.func.isRequired,
+};
+
+function useVisibilityToggle() {
+ const [request, setRequest] = useState(null);
+ const mx = initMatrix.matrixClient;
+
+ useEffect(() => {
+ const handleOpen = (req) => setRequest(req);
+ navigation.on(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
+ mx.on('crypto.verification.request', handleOpen);
+ return () => {
+ navigation.removeListener(cons.events.navigation.EMOJI_VERIFICATION_OPENED, handleOpen);
+ mx.removeListener('crypto.verification.request', handleOpen);
+ };
+ }, []);
+
+ const requestClose = () => setRequest(null);
+
+ return [request, requestClose];
+}
+
+function EmojiVerification() {
+ const [request, requestClose] = useVisibilityToggle();
+
+ return (
+ <Dialog
+ isOpen={request !== null}
+ className="emoji-verification"
+ title={(
+ <Text variant="s1" weight="medium" primary>
+ Emoji verification
+ </Text>
+ )}
+ contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
+ onRequestClose={requestClose}
+ >
+ {
+ request !== null
+ ? <EmojiVerificationContent request={request} requestClose={requestClose} />
+ : <div />
+ }
+ </Dialog>
+ );
+}
+
+export default EmojiVerification;
--- /dev/null
+@use '../../partials/flex';
+@use '../../partials/dir';
+
+.emoji-verification {
+ &__content {
+ padding: var(--sp-normal);
+ @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-normal);
+ }
+
+ &__emojis {
+ margin: var(--sp-loose) 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ gap: var(--sp-extra-tight);
+ flex-wrap: wrap;
+ }
+
+ &__emoji-block {
+ @extend .cp-fx__column;
+ flex: 1;
+ align-items: center;
+ gap: var(--sp-extra-tight);
+ white-space: nowrap;
+ text-transform: capitalize;
+ }
+
+ &__buttons {
+ display: flex;
+ gap: var(--sp-normal);
+ }
+}
import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom';
+import EmojiVerification from '../emoji-verification/EmojiVerification';
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
<CreateRoom />
<SpaceAddExisting />
<Search />
+ <EmojiVerification />
<ReusableDialog />
</>
import initMatrix from '../../../client/initMatrix';
import { isCrossVerified } from '../../../util/matrixUtil';
-import { openReusableDialog } from '../../../client/action/navigation';
+import { openReusableDialog, openEmojiVerification } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
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 [truncated, setTruncated] = useState(true);
const mountStore = useStore();
mountStore.setItem(true);
+ const isMeVerified = isCrossVerified(mx.deviceId);
useEffect(() => {
setProcessing([]);
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);
+ };
+
+ 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;
+
return (
<SettingTile
key={deviceId}
title={(
- <Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
+ <Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
{displayName}
- <Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
+ <Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
+ {isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
</Text>
)}
options={
? <Spinner size="small" />
: (
<>
+ {((isMeVerified && isVerified === false) || (isCurrentDevice && isVerified === false)) && <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={(
- <Text variant="b3">
- Last activity
- <span style={{ color: 'var(--tc-surface-normal)' }}>
- {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
- </span>
- {lastIP ? ` at ${lastIP}` : ''}
- </Text>
+ <>
+ <Text variant="b3">
+ Last activity
+ <span style={{ color: 'var(--tc-surface-normal)' }}>
+ {dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
+ </span>
+ {lastIP ? ` at ${lastIP}` : ''}
+ </Text>
+ {isCurrentDevice && (
+ <Text style={{ marginTop: 'var(--sp-ultra-tight)' }} variant="b3">
+ {`Session Key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
+ </Text>
+ )}
+ </>
)}
/>
);
{noEncryption.length > 0 && (
<div>
<MenuHeader>Sessions without encryption support</MenuHeader>
- {noEncryption.map((device) => renderDevice(device, true))}
+ {noEncryption.map((device) => renderDevice(device, null))}
</div>
)}
<div>
if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true);
})
- : <Text className="device-manage__info">No verified session</Text>
+ : <Text className="device-manage__info">No verified sessions</Text>
}
{ verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
& .setting-tile:last-of-type {
border-bottom: none;
}
+ & .setting-tile__options {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-ultra-tight);
+ & .btn-positive {
+ padding: 6px var(--sp-tight);
+ min-width: 0;
+ }
+ }
+
+ &__current-label {
+ margin: 0 var(--sp-extra-tight);
+ padding: 2px var(--sp-ultra-tight);
+ color: var(--bg-surface);
+ background-color: var(--tc-surface-low);
+ border-radius: 4px;
+ }
&__rename {
padding: var(--sp-normal);
afterClose,
});
}
+
+export function openEmojiVerification(request) {
+ appDispatcher.dispatch({
+ type: cons.actions.navigation.OPEN_EMOJI_VERIFICATION,
+ request,
+ });
+}
deviceId: secret.deviceId,
timelineSupport: true,
cryptoCallbacks,
+ verificationMethods: [
+ 'm.sas.v1',
+ ],
});
await this.matrixClient.initCrypto();
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_NAVIGATION: 'OPEN_NAVIGATION',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
+ OPEN_EMOJI_VERIFICATION: 'OPEN_EMOJI_VERIFICATION',
},
room: {
JOIN: 'JOIN',
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
NAVIGATION_OPENED: 'NAVIGATION_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
+ EMOJI_VERIFICATION_OPENED: 'EMOJI_VERIFICATION_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
action.afterClose,
);
},
+ [cons.actions.navigation.OPEN_EMOJI_VERIFICATION]: () => {
+ this.emit(
+ cons.events.navigation.EMOJI_VERIFICATION_OPENED,
+ action.request,
+ );
+ },
};
actions[action.type]?.();
}