diff --git a/src/app/organisms/emoji-verification/EmojiVerification.jsx b/src/app/organisms/emoji-verification/EmojiVerification.jsx
new file mode 100644
index 00000000..f56a4672
--- /dev/null
+++ b/src/app/organisms/emoji-verification/EmojiVerification.jsx
@@ -0,0 +1,153 @@
+/* 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 = () => (
+ <>
+
+ Waiting for response from other device...
+ >
+ );
+
+ if (sas !== null) {
+ return (
+
+
Confirm the emoji below are displayed on both devices, in the same order:
+
+ {sas.sas.emoji.map((emoji) => (
+
+ {twemojify(emoji[0])}
+ {emoji[1]}
+
+ ))}
+
+
+ {process ? renderWait() : (
+ <>
+
+
+ >
+ )}
+
+
+ );
+ }
+
+ return (
+
+
Click accept to start the verification process
+
+ {
+ process
+ ? renderWait()
+ :
+ }
+
+
+ );
+}
+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 (
+
+ );
+}
+
+export default EmojiVerification;
diff --git a/src/app/organisms/emoji-verification/EmojiVerification.scss b/src/app/organisms/emoji-verification/EmojiVerification.scss
new file mode 100644
index 00000000..4e6dc112
--- /dev/null
+++ b/src/app/organisms/emoji-verification/EmojiVerification.scss
@@ -0,0 +1,35 @@
+@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);
+ }
+}
diff --git a/src/app/organisms/pw/Dialogs.jsx b/src/app/organisms/pw/Dialogs.jsx
index f29a8192..28cb47ad 100644
--- a/src/app/organisms/pw/Dialogs.jsx
+++ b/src/app/organisms/pw/Dialogs.jsx
@@ -7,6 +7,7 @@ import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExistin
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';
@@ -20,6 +21,7 @@ function Dialogs() {
+
>
diff --git a/src/app/organisms/settings/DeviceManage.jsx b/src/app/organisms/settings/DeviceManage.jsx
index c47baa6b..a7409aa2 100644
--- a/src/app/organisms/settings/DeviceManage.jsx
+++ b/src/app/organisms/settings/DeviceManage.jsx
@@ -4,7 +4,7 @@ import dateFormat from 'dateformat';
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';
@@ -70,6 +70,7 @@ function DeviceManage() {
const [truncated, setTruncated] = useState(true);
const mountStore = useStore();
mountStore.setItem(true);
+ const isMeVerified = isCrossVerified(mx.deviceId);
useEffect(() => {
setProcessing([]);
@@ -129,18 +130,15 @@ function DeviceManage() {
};
const verifyWithKey = async (device) => {
- const keyData = await accessSecretStorage('Device Verification');
+ const keyData = await accessSecretStorage('Session verification');
if (!keyData) return;
addToProcessing(device);
await mx.checkOwnCrossSigningTrust();
};
- const verifyWithEmojis = async () => {
- // TODO:
- };
-
- const verifyManually = async () => {
- // TODO:
+ const verifyWithEmojis = async (deviceId) => {
+ const req = await mx.requestVerification(mx.getUserId(), [deviceId]);
+ openEmojiVerification(req);
};
const verify = (deviceId, isCurrentDevice) => {
@@ -148,7 +146,7 @@ function DeviceManage() {
verifyWithKey(deviceId);
return;
}
- verifyManually(deviceId);
+ verifyWithEmojis(deviceId);
};
const renderDevice = (device, isVerified) => {
@@ -163,8 +161,8 @@ function DeviceManage() {
key={deviceId}
title={(
- {displayName ? `${displayName} — ` : ''}
- {deviceId}
+ {displayName}
+ {`${displayName ? ' — ' : ''}${deviceId}`}
{isCurrentDevice && Current}
)}
@@ -173,20 +171,27 @@ function DeviceManage() {
?
: (
<>
- {isVerified === false && }
+ {((isMeVerified && isVerified === false) || (isCurrentDevice && isVerified === false)) && }
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}` : ''}
-
+ <>
+
+ 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(' ')}`}
+
+ )}
+ >
)}
/>
);
diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js
index e05cf511..9e859494 100644
--- a/src/client/action/navigation.js
+++ b/src/client/action/navigation.js
@@ -166,3 +166,10 @@ export function openReusableDialog(title, render, afterClose) {
afterClose,
});
}
+
+export function openEmojiVerification(request) {
+ appDispatcher.dispatch({
+ type: cons.actions.navigation.OPEN_EMOJI_VERIFICATION,
+ request,
+ });
+}
diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js
index 0e5cd0d0..aec2f3da 100644
--- a/src/client/initMatrix.js
+++ b/src/client/initMatrix.js
@@ -38,6 +38,9 @@ class InitMatrix extends EventEmitter {
deviceId: secret.deviceId,
timelineSupport: true,
cryptoCallbacks,
+ verificationMethods: [
+ 'm.sas.v1',
+ ],
});
await this.matrixClient.initCrypto();
diff --git a/src/client/state/cons.js b/src/client/state/cons.js
index 34a3a928..789ed587 100644
--- a/src/client/state/cons.js
+++ b/src/client/state/cons.js
@@ -49,6 +49,7 @@ const cons = {
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',
@@ -96,6 +97,7 @@ const cons = {
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',
diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js
index bbecb2e1..42ab0199 100644
--- a/src/client/state/navigation.js
+++ b/src/client/state/navigation.js
@@ -185,6 +185,12 @@ class Navigation extends EventEmitter {
action.afterClose,
);
},
+ [cons.actions.navigation.OPEN_EMOJI_VERIFICATION]: () => {
+ this.emit(
+ cons.events.navigation.EMOJI_VERIFICATION_OPENED,
+ action.request,
+ );
+ },
};
actions[action.type]?.();
}