From 0f7ef6912fc9d8c4fc3d87a1b33df4f78350c91d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 21 Mar 2023 20:56:06 +0530 Subject: [PATCH] WIP: Add views/view-models --- src/domain/navigation/index.ts | 4 +- src/domain/session/SessionViewModel.js | 25 +++++- .../session/settings/KeyBackupViewModel.ts | 4 + .../DeviceVerificationViewModel.ts | 89 +++++++++++++++++++ .../stages/SelectMethodViewModel.ts | 50 +++++++++++ .../stages/VerificationCancelledViewModel.ts | 38 ++++++++ .../stages/VerificationCompleteViewModel.ts | 35 ++++++++ .../stages/VerifyEmojisViewModel.ts | 42 +++++++++ .../stages/WaitingForOtherUserViewModel.ts | 29 ++++++ .../verification/SAS/SASVerification.ts | 7 ++ .../verification/SAS/channel/Channel.ts | 24 +++-- .../verification/SAS/channel/MockChannel.ts | 1 + .../stages/SelectVerificationMethodStage.ts | 9 ++ .../verification/SAS/stages/SendDoneStage.ts | 3 +- src/matrix/verification/SAS/types.ts | 3 + .../ui/css/themes/element/icons/verified.svg | 2 +- .../web/ui/css/themes/element/theme.css | 88 ++++++++++++++++++ src/platform/web/ui/session/SessionView.js | 3 + .../session/settings/KeyBackupSettingsView.ts | 23 +++-- .../verification/DeviceVerificationView.ts | 57 ++++++++++++ .../verification/stages/SelectMethodView.ts | 62 +++++++++++++ .../stages/VerificationCancelledView.ts | 81 +++++++++++++++++ .../stages/VerificationCompleteView.ts | 45 ++++++++++ .../verification/stages/VerifyEmojisView.ts | 79 ++++++++++++++++ .../stages/WaitingForOtherUserView.ts | 46 ++++++++++ 25 files changed, 831 insertions(+), 18 deletions(-) create mode 100644 src/domain/session/verification/DeviceVerificationViewModel.ts create mode 100644 src/domain/session/verification/stages/SelectMethodViewModel.ts create mode 100644 src/domain/session/verification/stages/VerificationCancelledViewModel.ts create mode 100644 src/domain/session/verification/stages/VerificationCompleteViewModel.ts create mode 100644 src/domain/session/verification/stages/VerifyEmojisViewModel.ts create mode 100644 src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts create mode 100644 src/platform/web/ui/session/verification/DeviceVerificationView.ts create mode 100644 src/platform/web/ui/session/verification/stages/SelectMethodView.ts create mode 100644 src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts create mode 100644 src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts create mode 100644 src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts create mode 100644 src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index a2705944..c8f62a7b 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -34,6 +34,8 @@ export type SegmentType = { "details": true; "members": true; "member": string; + "device-verification": true; + "join-room": true; }; export function createNavigation(): Navigation { @@ -51,7 +53,7 @@ function allowsChild(parent: Segment | undefined, child: Segment { + this._updateVerification(verificationOpen); + })); + this._updateVerification(verification.get()); + const lightbox = this.navigation.observe("lightbox"); this.track(lightbox.subscribe(eventId => { this._updateLightbox(eventId); @@ -143,7 +151,8 @@ export class SessionViewModel extends ViewModel { this._gridViewModel || this._settingsViewModel || this._createRoomViewModel || - this._joinRoomViewModel + this._joinRoomViewModel || + this._verificationViewModel ); } @@ -179,6 +188,10 @@ export class SessionViewModel extends ViewModel { return this._joinRoomViewModel; } + get verificationViewModel() { + return this._verificationViewModel; + } + get toastCollectionViewModel() { return this._toastCollectionViewModel; } @@ -327,6 +340,16 @@ export class SessionViewModel extends ViewModel { this.emitChange("activeMiddleViewModel"); } + _updateVerification(verificationOpen) { + if (this._verificationViewModel) { + this._verificationViewModel = this.disposeTracked(this._verificationViewModel); + } + if (verificationOpen) { + this._verificationViewModel = this.track(new DeviceVerificationViewModel(this.childOptions({ session: this._client.session }))); + } + this.emitChange("activeMiddleViewModel"); + } + _updateLightbox(eventId) { if (this._lightboxViewModel) { this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); diff --git a/src/domain/session/settings/KeyBackupViewModel.ts b/src/domain/session/settings/KeyBackupViewModel.ts index 663d1ea6..43681a29 100644 --- a/src/domain/session/settings/KeyBackupViewModel.ts +++ b/src/domain/session/settings/KeyBackupViewModel.ts @@ -157,6 +157,10 @@ export class KeyBackupViewModel extends ViewModel { } } + navigateToVerification(): void { + this.navigation.push("device-verification", true); + } + get backupWriteStatus(): BackupWriteStatus { const keyBackup = this._keyBackup; if (!keyBackup || keyBackup.version === undefined) { diff --git a/src/domain/session/verification/DeviceVerificationViewModel.ts b/src/domain/session/verification/DeviceVerificationViewModel.ts new file mode 100644 index 00000000..2c471ca5 --- /dev/null +++ b/src/domain/session/verification/DeviceVerificationViewModel.ts @@ -0,0 +1,89 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {Options as BaseOptions} from "../../ViewModel"; +import {SegmentType} from "../../navigation/index"; +import {ErrorReportViewModel} from "../../ErrorReportViewModel"; +import {WaitingForOtherUserViewModel} from "./stages/WaitingForOtherUserViewModel"; +import {VerificationCancelledViewModel} from "./stages/VerificationCancelledViewModel"; +import {SelectMethodViewModel} from "./stages/SelectMethodViewModel"; +import {VerifyEmojisViewModel} from "./stages/VerifyEmojisViewModel"; +import {VerificationCompleteViewModel} from "./stages/VerificationCompleteViewModel"; +import type {Session} from "../../../matrix/Session.js"; +import type {SASVerification} from "../../../matrix/verification/SAS/SASVerification"; + +type Options = BaseOptions & { + session: Session; +}; + +export class DeviceVerificationViewModel extends ErrorReportViewModel { + private session: Session; + private sas: SASVerification; + private _currentStageViewModel: any; + + constructor(options: Readonly) { + super(options); + this.session = options.session; + this.createAndStartSasVerification(); + this._currentStageViewModel = this.track( + new WaitingForOtherUserViewModel( + this.childOptions({ sas: this.sas }) + ) + ); + } + + async createAndStartSasVerification(): Promise { + await this.logAndCatch("DeviceVerificationViewModel.createAndStartSasVerification", (log) => { + // todo: can crossSigning be undefined? + const crossSigning = this.session.crossSigning; + // todo: should be called createSasVerification + this.sas = crossSigning.startVerification(this.session.userId, undefined, log); + const emitter = this.sas.eventEmitter; + this.track(emitter.disposableOn("SelectVerificationStage", (stage) => { + this.createViewModelAndEmit( + new SelectMethodViewModel(this.childOptions({ sas: this.sas, stage: stage!, })) + ); + })); + this.track(emitter.disposableOn("EmojiGenerated", (stage) => { + this.createViewModelAndEmit( + new VerifyEmojisViewModel(this.childOptions({ stage: stage!, })) + ); + })); + this.track(emitter.disposableOn("VerificationCancelled", (cancellation) => { + this.createViewModelAndEmit( + new VerificationCancelledViewModel( + this.childOptions({ cancellationCode: cancellation!.code, cancelledByUs: cancellation!.cancelledByUs, }) + )); + })); + this.track(emitter.disposableOn("VerificationCompleted", (deviceId) => { + this.createViewModelAndEmit( + new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! })) + ); + })); + return this.sas.start(); + }); + } + + private createViewModelAndEmit(vm) { + this._currentStageViewModel = this.disposeTracked(this._currentStageViewModel); + this._currentStageViewModel = this.track(vm); + this.emitChange("currentStageViewModel"); + } + + get currentStageViewModel() { + return this._currentStageViewModel; + } +} diff --git a/src/domain/session/verification/stages/SelectMethodViewModel.ts b/src/domain/session/verification/stages/SelectMethodViewModel.ts new file mode 100644 index 00000000..681a2e46 --- /dev/null +++ b/src/domain/session/verification/stages/SelectMethodViewModel.ts @@ -0,0 +1,50 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {SegmentType} from "../../../navigation/index"; +import {ErrorReportViewModel} from "../../../ErrorReportViewModel"; +import type {Options as BaseOptions} from "../../../ViewModel"; +import type {Session} from "../../../../matrix/Session.js"; +import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification"; +import type {SelectVerificationMethodStage} from "../../../../matrix/verification/SAS/stages/SelectVerificationMethodStage"; + +type Options = BaseOptions & { + sas: SASVerification; + stage: SelectVerificationMethodStage; + session: Session; +}; + +export class SelectMethodViewModel extends ErrorReportViewModel { + public hasProceeded: boolean = false; + + async proceed() { + await this.logAndCatch("SelectMethodViewModel.proceed", async (log) => { + await this.options.stage.selectEmojiMethod(log); + this.hasProceeded = true; + this.emitChange("hasProceeded"); + }); + } + + async cancel() { + await this.logAndCatch("SelectMethodViewModel.cancel", async () => { + await this.options.sas.abort(); + }); + } + + get deviceName() { + return this.options.stage.otherDeviceName; + } +} diff --git a/src/domain/session/verification/stages/VerificationCancelledViewModel.ts b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts new file mode 100644 index 00000000..ad01d312 --- /dev/null +++ b/src/domain/session/verification/stages/VerificationCancelledViewModel.ts @@ -0,0 +1,38 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel, Options as BaseOptions} from "../../../ViewModel"; +import {SegmentType} from "../../../navigation/index"; +import {CancelTypes} from "../../../../matrix/verification/SAS/channel/types"; + +type Options = BaseOptions & { + cancellationCode: CancelTypes; + cancelledByUs: boolean; +}; + +export class VerificationCancelledViewModel extends ViewModel { + get cancelCode(): CancelTypes { + return this.options.cancellationCode; + } + + get isCancelledByUs(): boolean { + return this.options.cancelledByUs; + } + + gotoSettings() { + this.navigation.push("settings", true); + } +} diff --git a/src/domain/session/verification/stages/VerificationCompleteViewModel.ts b/src/domain/session/verification/stages/VerificationCompleteViewModel.ts new file mode 100644 index 00000000..a6982eb9 --- /dev/null +++ b/src/domain/session/verification/stages/VerificationCompleteViewModel.ts @@ -0,0 +1,35 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {SegmentType} from "../../../navigation/index"; +import {ErrorReportViewModel} from "../../../ErrorReportViewModel"; +import type {Options as BaseOptions} from "../../../ViewModel"; +import type {Session} from "../../../../matrix/Session.js"; + +type Options = BaseOptions & { + deviceId: string; + session: Session; +}; + +export class VerificationCompleteViewModel extends ErrorReportViewModel { + get otherDeviceId(): string { + return this.options.deviceId; + } + + gotoSettings() { + this.navigation.push("settings", true); + } +} diff --git a/src/domain/session/verification/stages/VerifyEmojisViewModel.ts b/src/domain/session/verification/stages/VerifyEmojisViewModel.ts new file mode 100644 index 00000000..14868176 --- /dev/null +++ b/src/domain/session/verification/stages/VerifyEmojisViewModel.ts @@ -0,0 +1,42 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {SegmentType} from "../../../navigation/index"; +import {ErrorReportViewModel} from "../../../ErrorReportViewModel"; +import type {Options as BaseOptions} from "../../../ViewModel"; +import type {Session} from "../../../../matrix/Session.js"; +import type {CalculateSASStage} from "../../../../matrix/verification/SAS/stages/CalculateSASStage"; + +type Options = BaseOptions & { + stage: CalculateSASStage; + session: Session; +}; + +export class VerifyEmojisViewModel extends ErrorReportViewModel { + public isWaiting: boolean = false; + + async setEmojiMatch(match: boolean) { + await this.logAndCatch("VerifyEmojisViewModel.setEmojiMatch", async () => { + await this.options.stage.setEmojiMatch(match); + this.isWaiting = true; + this.emitChange("isWaiting"); + }); + } + + get emojis() { + return this.options.stage.emoji; + } +} diff --git a/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts b/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts new file mode 100644 index 00000000..408ef884 --- /dev/null +++ b/src/domain/session/verification/stages/WaitingForOtherUserViewModel.ts @@ -0,0 +1,29 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel, Options as BaseOptions} from "../../../ViewModel"; +import {SegmentType} from "../../../navigation/index"; +import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification"; + +type Options = BaseOptions & { + sas: SASVerification; +}; + +export class WaitingForOtherUserViewModel extends ViewModel { + async cancel() { + await this.options.sas.abort(); + } +} diff --git a/src/matrix/verification/SAS/SASVerification.ts b/src/matrix/verification/SAS/SASVerification.ts index 5850ba5d..42a7e47d 100644 --- a/src/matrix/verification/SAS/SASVerification.ts +++ b/src/matrix/verification/SAS/SASVerification.ts @@ -84,6 +84,10 @@ export class SASVerification extends EventEmitter { } } + async abort() { + await this.channel.cancelVerification(CancelTypes.UserCancelled); + } + async start() { try { let stage = this.startStage; @@ -98,6 +102,9 @@ export class SASVerification extends EventEmitter { } } finally { + if (this.channel.isCancelled) { + this.eventEmitter.emit("VerificationCancelled", this.channel.cancellation); + } this.olmSas.free(); this.timeout.abort(); this.finished = true; diff --git a/src/matrix/verification/SAS/channel/Channel.ts b/src/matrix/verification/SAS/channel/Channel.ts index 6492a6ae..9763b95c 100644 --- a/src/matrix/verification/SAS/channel/Channel.ts +++ b/src/matrix/verification/SAS/channel/Channel.ts @@ -49,6 +49,8 @@ export interface IChannel { acceptMessage: any; startMessage: any; initiatedByUs: boolean; + isCancelled: boolean; + cancellation: { code: CancelTypes, cancelledByUs: boolean }; id: string; otherUserDeviceId: string; } @@ -78,7 +80,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { public startMessage: any; public id: string; private _initiatedByUs: boolean; - private _isCancelled = false; + private _cancellation: { code: CancelTypes, cancelledByUs: boolean }; /** * @@ -116,8 +118,12 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } + get cancellation() { + return this._cancellation; + }; + get isCancelled(): boolean { - return this._isCancelled; + return !!this._cancellation; } async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise { @@ -198,8 +204,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { this.handleReadyMessage(event, log); return; } - if (event.type === VerificationEventType.Cancel) { - this._isCancelled = true; + if (event.type === VerificationEventTypes.Cancel) { + this._cancellation = { code: event.content.code, cancelledByUs: false }; this.dispose(); return; } @@ -234,7 +240,7 @@ export class ToDeviceChannel extends Disposables implements IChannel { const payload = { messages: { [this.otherUserId]: { - [this.otherUserDeviceId]: { + [this.otherUserDeviceId ?? "*"]: { code: cancellationType, reason: messageFromErrorType[cancellationType], transaction_id: this.id, @@ -242,8 +248,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } } - await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response(); - this._isCancelled = true; + await this.hsApi.sendToDevice(VerificationEventTypes.Cancel, payload, makeTxnId(), { log }).response(); + this._cancellation = { code: cancellationType, cancelledByUs: true }; this.dispose(); }); } @@ -257,8 +263,8 @@ export class ToDeviceChannel extends Disposables implements IChannel { } } - waitForEvent(eventType: VerificationEventType): Promise { - if (this._isCancelled) { + waitForEvent(eventType: VerificationEventTypes): Promise { + if (this.isCancelled) { throw new VerificationCancelledError(); } // Check if we already received the message diff --git a/src/matrix/verification/SAS/channel/MockChannel.ts b/src/matrix/verification/SAS/channel/MockChannel.ts index 50197ba4..9553a92d 100644 --- a/src/matrix/verification/SAS/channel/MockChannel.ts +++ b/src/matrix/verification/SAS/channel/MockChannel.ts @@ -17,6 +17,7 @@ export class MockChannel implements ITestChannel { public initiatedByUs: boolean; public startMessage: any; public isCancelled: boolean = false; + public cancellation: { code: CancelTypes; cancelledByUs: boolean; }; private olmSas: any; constructor( diff --git a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts index da6099ee..db499d2e 100644 --- a/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts +++ b/src/matrix/verification/SAS/stages/SelectVerificationMethodStage.ts @@ -23,9 +23,11 @@ import type {ILogItem} from "../../../../logging/types"; export class SelectVerificationMethodStage extends BaseSASVerificationStage { private hasSentStartMessage = false; private allowSelection = true; + public otherDeviceName: string; async completeStage() { await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => { + await this.findDeviceName(log); this.eventEmitter.emit("SelectVerificationStage", this); const startMessage = this.channel.waitForEvent(VerificationEventType.Start); const acceptMessage = this.channel.waitForEvent(VerificationEventType.Accept); @@ -81,6 +83,13 @@ export class SelectVerificationMethodStage extends BaseSASVerificationStage { }); } + private async findDeviceName(log: ILogItem) { + await log.wrap("SelectVerificationMethodStage.findDeviceName", async () => { + const device = await this.options.deviceTracker.deviceForId(this.otherUserId, this.otherUserDeviceId, this.options.hsApi, log); + this.otherDeviceName = device.displayName; + }) + } + async selectEmojiMethod(log: ILogItem) { if (!this.allowSelection) { return; } const content = { diff --git a/src/matrix/verification/SAS/stages/SendDoneStage.ts b/src/matrix/verification/SAS/stages/SendDoneStage.ts index 2d3195b1..53b8a37e 100644 --- a/src/matrix/verification/SAS/stages/SendDoneStage.ts +++ b/src/matrix/verification/SAS/stages/SendDoneStage.ts @@ -19,7 +19,8 @@ import {VerificationEventType} from "../channel/types"; export class SendDoneStage extends BaseSASVerificationStage { async completeStage() { await this.log.wrap("SendDoneStage.completeStage", async (log) => { - await this.channel.send(VerificationEventType.Done, {}, log); + this.eventEmitter.emit("VerificationCompleted", this.otherUserDeviceId); + await this.channel.send(VerificationEventTypes.Done, {}, log); }); } } diff --git a/src/matrix/verification/SAS/types.ts b/src/matrix/verification/SAS/types.ts index d7be6921..3bfd742e 100644 --- a/src/matrix/verification/SAS/types.ts +++ b/src/matrix/verification/SAS/types.ts @@ -13,10 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import {CancelTypes} from "./channel/types"; import {CalculateSASStage} from "./stages/CalculateSASStage"; import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage"; export type SASProgressEvents = { SelectVerificationStage: SelectVerificationMethodStage; EmojiGenerated: CalculateSASStage; + VerificationCompleted: string; + VerificationCancelled: { code: CancelTypes, cancelledByUs: boolean }; } diff --git a/src/platform/web/ui/css/themes/element/icons/verified.svg b/src/platform/web/ui/css/themes/element/icons/verified.svg index 340891f1..d158e607 100644 --- a/src/platform/web/ui/css/themes/element/icons/verified.svg +++ b/src/platform/web/ui/css/themes/element/icons/verified.svg @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index ca64e15a..3f2c19e3 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1354,3 +1354,91 @@ button.RoomDetailsView_row::after { width: 100px; height: 40px; } + +.VerificationCompleteView, +.DeviceVerificationView, +.SelectMethodView { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.VerificationCompleteView__heading, +.VerifyEmojisView__heading, +.SelectMethodView__heading, +.WaitingForOtherUserView__heading { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + justify-content: center; + padding: 8px; +} + +.VerificationCompleteView>*, +.SelectMethodView>*, +.VerifyEmojisView>*, +.WaitingForOtherUserView>* { + padding: 16px; +} + +.VerificationCompleteView__title, +.VerifyEmojisView__title, +.SelectMethodView__title, +.WaitingForOtherUserView__title { + text-align: center; + margin: 0; +} + +.VerificationCancelledView__description, +.VerificationCompleteView__description, +.VerifyEmojisView__description, +.SelectMethodView__description, +.WaitingForOtherUserView__description { + text-align: center; + margin: 0; +} + +.VerificationCancelledView__actions, +.SelectMethodView__actions, +.VerifyEmojisView__actions, +.WaitingForOtherUserView__actions { + display: flex; + justify-content: center; + gap: 12px; + padding: 16px; +} + +.EmojiCollection { + display: flex; + justify-content: center; + gap: 16px; +} + +.EmojiContainer__emoji { + font-size: 3.2rem; +} + +.VerifyEmojisView__waiting, +.EmojiContainer__name, +.EmojiContainer__emoji { + display: flex; + justify-content: center; + align-items: center; +} + +.EmojiContainer__name { + font-weight: bold; +} + +.VerifyEmojisView__waiting { + gap: 12px; +} + +.VerificationCompleteView__icon { + background: url("./icons//verified.svg?primary=accent-color") no-repeat; + background-size: contain; + width: 128px; + height: 128px; +} diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 9f84e872..8156085c 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -30,6 +30,7 @@ import {CreateRoomView} from "./CreateRoomView.js"; import {RightPanelView} from "./rightpanel/RightPanelView.js"; import {viewClassForTile} from "./room/common"; import {JoinRoomView} from "./JoinRoomView"; +import {DeviceVerificationView} from "./verification/DeviceVerificationView"; import {ToastCollectionView} from "./toast/ToastCollectionView"; export class SessionView extends TemplateView { @@ -53,6 +54,8 @@ export class SessionView extends TemplateView { return new CreateRoomView(vm.createRoomViewModel); } else if (vm.joinRoomViewModel) { return new JoinRoomView(vm.joinRoomViewModel); + } else if (vm.verificationViewModel) { + return new DeviceVerificationView(vm.verificationViewModel); } else if (vm.currentRoomViewModel) { if (vm.currentRoomViewModel.kind === "invite") { return new InviteView(vm.currentRoomViewModel); diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts index 668f4f17..7c3d6491 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.ts @@ -62,11 +62,24 @@ export class KeyBackupSettingsView extends TemplateView { return t.p("Cross-signing master key found and trusted.") }), t.if(vm => vm.canSignOwnDevice, t => { - return t.button({ - onClick: disableTargetCallback(async () => { - await vm.signOwnDevice(); - }) - }, "Sign own device"); + return t.div([ + t.button( + { + onClick: disableTargetCallback(async (evt) => { + await vm.signOwnDevice(); + }), + }, + "Sign own device" + ), + t.button( + { + onClick: disableTargetCallback(async () => { + vm.navigateToVerification(); + }), + }, + "Verify by emoji" + ), + ]); }), ]); diff --git a/src/platform/web/ui/session/verification/DeviceVerificationView.ts b/src/platform/web/ui/session/verification/DeviceVerificationView.ts new file mode 100644 index 00000000..ed599031 --- /dev/null +++ b/src/platform/web/ui/session/verification/DeviceVerificationView.ts @@ -0,0 +1,57 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../../general/TemplateView"; +import {WaitingForOtherUserViewModel} from "../../../../../domain/session/verification/stages/WaitingForOtherUserViewModel"; +import {DeviceVerificationViewModel} from "../../../../../domain/session/verification/DeviceVerificationViewModel"; +import {VerificationCancelledViewModel} from "../../../../../domain/session/verification/stages/VerificationCancelledViewModel"; +import {WaitingForOtherUserView} from "./stages/WaitingForOtherUserView"; +import {VerificationCancelledView} from "./stages/VerificationCancelledView"; +import {SelectMethodViewModel} from "../../../../../domain/session/verification/stages/SelectMethodViewModel"; +import {SelectMethodView} from "./stages/SelectMethodView"; +import {VerifyEmojisViewModel} from "../../../../../domain/session/verification/stages/VerifyEmojisViewModel"; +import {VerifyEmojisView} from "./stages/VerifyEmojisView"; +import {VerificationCompleteViewModel} from "../../../../../domain/session/verification/stages/VerificationCompleteViewModel"; +import {VerificationCompleteView} from "./stages/VerificationCompleteView"; + +export class DeviceVerificationView extends TemplateView { + render(t, vm) { + return t.div({ + className: { + "middle": true, + "DeviceVerificationView": true, + } + }, [ + t.mapView(vm => vm.currentStageViewModel, (stageVm) => { + if (stageVm instanceof WaitingForOtherUserViewModel) { + return new WaitingForOtherUserView(stageVm); + } + else if (stageVm instanceof VerificationCancelledViewModel) { + return new VerificationCancelledView(stageVm); + } + else if (stageVm instanceof SelectMethodViewModel) { + return new SelectMethodView(stageVm); + } + else if (stageVm instanceof VerifyEmojisViewModel) { + return new VerifyEmojisView(stageVm); + } + else if (stageVm instanceof VerificationCompleteViewModel) { + return new VerificationCompleteView(stageVm); + } + }) + ]) + } +} diff --git a/src/platform/web/ui/session/verification/stages/SelectMethodView.ts b/src/platform/web/ui/session/verification/stages/SelectMethodView.ts new file mode 100644 index 00000000..9e665f31 --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/SelectMethodView.ts @@ -0,0 +1,62 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../../../general/TemplateView"; +import {spinner} from "../../../common.js" +import type {SelectMethodViewModel} from "../../../../../../domain/session/verification/stages/SelectMethodViewModel"; + +export class SelectMethodView extends TemplateView { + render(t) { + return t.div({ className: "SelectMethodView" }, [ + t.map(vm => vm.hasProceeded, (hasProceeded, t, vm) => { + if (hasProceeded) { + return spinner(t); + } + else return t.div([ + t.div({ className: "SelectMethodView__heading" }, [ + t.h2( { className: "SelectMethodView__title" }, vm.i18n`Verify device '${vm.deviceName}' by comparing emojis?`), + ]), + t.p({ className: "SelectMethodView__description" }, + vm.i18n`You are about to verify your other device by comparing emojis.` + ), + t.div({ className: "SelectMethodView__actions" }, [ + t.button( + { + className: { + "button-action": true, + primary: true, + destructive: true, + }, + onclick: () => vm.cancel(), + }, + "Cancel" + ), + t.button( + { + className: { + "button-action": true, + primary: true, + }, + onclick: () => vm.proceed(), + }, + "Proceed" + ), + ]), + ]); + }), + ]); + } +} diff --git a/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts new file mode 100644 index 00000000..d2afa98d --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/VerificationCancelledView.ts @@ -0,0 +1,81 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../../../general/TemplateView"; +import {VerificationCancelledViewModel} from "../../../../../../domain/session/verification/stages/VerificationCancelledViewModel"; +import {CancelTypes} from "../../../../../../matrix/verification/SAS/channel/types"; + +export class VerificationCancelledView extends TemplateView { + render(t, vm: VerificationCancelledViewModel) { + const headerTextStart = vm.isCancelledByUs ? "You" : "The other device"; + + return t.div( + { + className: "VerificationCancelledView", + }, + [ + t.h2( + { className: "VerificationCancelledView__title" }, + vm.i18n`${headerTextStart} cancelled the verification!` + ), + t.p( + { className: "VerificationCancelledView__description" }, + vm.i18n`${this.getDescriptionFromCancellationCode(vm.cancelCode, vm.isCancelledByUs)}` + ), + t.div({ className: "VerificationCancelledView__actions" }, [ + t.button({ + className: { + "button-action": true, + "primary": true, + }, + onclick: () => vm.gotoSettings(), + }, "Got it") + ]), + ] + ); + } + + getDescriptionFromCancellationCode(code: CancelTypes, isCancelledByUs: boolean): string { + const descriptionsWhenWeCancelled = { + // [CancelTypes.UserCancelled]: NO_NEED_FOR_DESCRIPTION_HERE + [CancelTypes.InvalidMessage]: "You other device sent an invalid message.", + [CancelTypes.KeyMismatch]: "The key could not be verified.", + // [CancelTypes.OtherDeviceAccepted]: "Another device has accepted this request.", + [CancelTypes.TimedOut]: "The verification process timed out.", + [CancelTypes.UnexpectedMessage]: "Your other device sent an unexpected message.", + [CancelTypes.UnknownMethod]: "Your other device is using an unknown method for verification.", + [CancelTypes.UnknownTransaction]: "Your other device sent a message with an unknown transaction id.", + [CancelTypes.UserMismatch]: "The expected user did not match the user verified.", + [CancelTypes.MismatchedCommitment]: "The hash commitment does not match.", + [CancelTypes.MismatchedSAS]: "The emoji/decimal did not match.", + } + const descriptionsWhenTheyCancelled = { + [CancelTypes.UserCancelled]: "Your other device cancelled the verification!", + [CancelTypes.InvalidMessage]: "Invalid message sent to the other device.", + [CancelTypes.KeyMismatch]: "The other device could not verify our keys", + // [CancelTypes.OtherDeviceAccepted]: "Another device has accepted this request.", + [CancelTypes.TimedOut]: "The verification process timed out.", + [CancelTypes.UnexpectedMessage]: "Unexpected message sent to the other device.", + [CancelTypes.UnknownMethod]: "Your other device does not understand the method you chose", + [CancelTypes.UnknownTransaction]: "Your other device rejected our message.", + [CancelTypes.UserMismatch]: "The expected user did not match the user verified.", + [CancelTypes.MismatchedCommitment]: "Your other device was not able to verify the hash commitment", + [CancelTypes.MismatchedSAS]: "The emoji/decimal did not match.", + } + const map = isCancelledByUs ? descriptionsWhenWeCancelled : descriptionsWhenTheyCancelled; + return map[code] ?? ""; + } +} diff --git a/src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts b/src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts new file mode 100644 index 00000000..7edc4e40 --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/VerificationCompleteView.ts @@ -0,0 +1,45 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../../../general/TemplateView"; +import type {VerificationCompleteViewModel} from "../../../../../../domain/session/verification/stages/VerificationCompleteViewModel"; + +export class VerificationCompleteView extends TemplateView { + render(t, vm: VerificationCompleteViewModel) { + return t.div({ className: "VerificationCompleteView" }, [ + t.div({className: "VerificationCompleteView__icon"}), + t.div({ className: "VerificationCompleteView__heading" }, [ + t.h2( + { className: "VerificationCompleteView__title" }, + vm.i18n`Verification completed successfully!` + ), + ]), + t.p( + { className: "VerificationCompleteView__description" }, + vm.i18n`You successfully verified device ${vm.otherDeviceId}` + ), + t.div({ className: "VerificationCompleteView__actions" }, [ + t.button({ + className: { + "button-action": true, + "primary": true, + }, + onclick: () => vm.gotoSettings(), + }, "Got it") + ]), + ]); + } +} diff --git a/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts b/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts new file mode 100644 index 00000000..9f7b312b --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/VerifyEmojisView.ts @@ -0,0 +1,79 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../../../general/TemplateView"; +import {spinner} from "../../../common.js" +import type {VerifyEmojisViewModel} from "../../../../../../domain/session/verification/stages/VerifyEmojisViewModel"; + +export class VerifyEmojisView extends TemplateView { + render(t, vm: VerifyEmojisViewModel) { + const emojiList = vm.emojis.reduce((acc, [emoji, name]) => { + const e = t.div({ className: "EmojiContainer" }, [ + t.div({ className: "EmojiContainer__emoji" }, emoji), + t.div({ className: "EmojiContainer__name" }, name), + ]); + acc.push(e); + return acc; + }, [] as any); + const emojiCollection = t.div({ className: "EmojiCollection" }, emojiList); + return t.div({ className: "VerifyEmojisView" }, [ + t.div({ className: "VerifyEmojisView__heading" }, [ + t.h2( + { className: "VerifyEmojisView__title" }, + vm.i18n`Do the emojis match?` + ), + ]), + t.p( + { className: "VerifyEmojisView__description" }, + vm.i18n`Confirm the emoji below are displayed on both devices, in the same order:` + ), + t.div({ className: "VerifyEmojisView__emojis" }, emojiCollection), + t.map(vm => vm.isWaiting, (isWaiting, t, vm) => { + if (isWaiting) { + return t.div({ className: "VerifyEmojisView__waiting" }, [ + spinner(t), + t.span(vm.i18n`Waiting for you to verify on your other device`), + ]); + } + else { + return t.div({ className: "VerifyEmojisView__actions" }, [ + t.button( + { + className: { + "button-action": true, + primary: true, + destructive: true, + }, + onclick: () => vm.setEmojiMatch(false), + }, + vm.i18n`They don't match` + ), + t.button( + { + className: { + "button-action": true, + primary: true, + }, + onclick: () => vm.setEmojiMatch(true), + }, + vm.i18n`They match` + ), + ]); + } + }) + ]); + } +} diff --git a/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts b/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts new file mode 100644 index 00000000..007b258e --- /dev/null +++ b/src/platform/web/ui/session/verification/stages/WaitingForOtherUserView.ts @@ -0,0 +1,46 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../../../general/TemplateView"; +import {spinner} from "../../../common.js"; +import {WaitingForOtherUserViewModel} from "../../../../../../domain/session/verification/stages/WaitingForOtherUserViewModel"; + +export class WaitingForOtherUserView extends TemplateView { + render(t, vm) { + return t.div({ className: "WaitingForOtherUserView" }, [ + t.div({ className: "WaitingForOtherUserView__heading" }, [ + spinner(t), + t.h2( + { className: "WaitingForOtherUserView__title" }, + vm.i18n`Waiting for any of your device to accept the verification request` + ), + ]), + t.p({ className: "WaitingForOtherUserView__description" }, + vm.i18n`Accept the request from the device you wish to verify!` + ), + t.div({ className: "WaitingForOtherUserView__actions" }, + t.button({ + className: { + "button-action": true, + "primary": true, + "destructive": true, + }, + onclick: () => vm.cancel(), + }, "Cancel") + ), + ]); + } +}