WIP: Add views/view-models

This commit is contained in:
RMidhunSuresh 2023-03-21 20:56:06 +05:30
parent c92fd6069d
commit 0f7ef6912f
25 changed files with 831 additions and 18 deletions

View File

@ -34,6 +34,8 @@ export type SegmentType = {
"details": true;
"members": true;
"member": string;
"device-verification": true;
"join-room": true;
};
export function createNavigation(): Navigation<SegmentType> {
@ -51,7 +53,7 @@ function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<Se
// allowed root segments
return type === "login" || type === "session" || type === "sso" || type === "logout";
case "session":
return type === "room" || type === "rooms" || type === "settings" || type === "create-room" || type === "join-room";
return type === "room" || type === "rooms" || type === "settings" || type === "create-room" || type === "join-room" || type === "device-verification";
case "rooms":
// downside of the approach: both of these will control which tile is selected
return type === "room" || type === "empty-grid-tile";

View File

@ -26,6 +26,7 @@ import {RoomGridViewModel} from "./RoomGridViewModel.js";
import {SettingsViewModel} from "./settings/SettingsViewModel.js";
import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
import {JoinRoomViewModel} from "./JoinRoomViewModel";
import {DeviceVerificationViewModel} from "./verification/DeviceVerificationViewModel";
import {ViewModel} from "../ViewModel";
import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";
@ -48,6 +49,7 @@ export class SessionViewModel extends ViewModel {
this._gridViewModel = null;
this._createRoomViewModel = null;
this._joinRoomViewModel = null;
this._verificationViewModel = null;
this._toastCollectionViewModel = this.track(new ToastCollectionViewModel(this.childOptions({
session: this._client.session,
})));
@ -95,6 +97,12 @@ export class SessionViewModel extends ViewModel {
}));
this._updateJoinRoom(joinRoom.get());
const verification = this.navigation.observe("device-verification");
this.track(verification.subscribe((verificationOpen) => {
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);

View File

@ -157,6 +157,10 @@ export class KeyBackupViewModel extends ViewModel<SegmentType, Options> {
}
}
navigateToVerification(): void {
this.navigation.push("device-verification", true);
}
get backupWriteStatus(): BackupWriteStatus {
const keyBackup = this._keyBackup;
if (!keyBackup || keyBackup.version === undefined) {

View File

@ -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<SegmentType, Options> {
private session: Session;
private sas: SASVerification;
private _currentStageViewModel: any;
constructor(options: Readonly<Options>) {
super(options);
this.session = options.session;
this.createAndStartSasVerification();
this._currentStageViewModel = this.track(
new WaitingForOtherUserViewModel(
this.childOptions({ sas: this.sas })
)
);
}
async createAndStartSasVerification(): Promise<void> {
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;
}
}

View File

@ -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<SegmentType, Options> {
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;
}
}

View File

@ -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<SegmentType, Options> {
get cancelCode(): CancelTypes {
return this.options.cancellationCode;
}
get isCancelledByUs(): boolean {
return this.options.cancelledByUs;
}
gotoSettings() {
this.navigation.push("settings", true);
}
}

View File

@ -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<SegmentType, Options> {
get otherDeviceId(): string {
return this.options.deviceId;
}
gotoSettings() {
this.navigation.push("settings", true);
}
}

View File

@ -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<SegmentType, Options> {
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;
}
}

View File

@ -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<SegmentType, Options> {
async cancel() {
await this.options.sas.abort();
}
}

View File

@ -84,6 +84,10 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
}
}
async abort() {
await this.channel.cancelVerification(CancelTypes.UserCancelled);
}
async start() {
try {
let stage = this.startStage;
@ -98,6 +102,9 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
}
}
finally {
if (this.channel.isCancelled) {
this.eventEmitter.emit("VerificationCancelled", this.channel.cancellation);
}
this.olmSas.free();
this.timeout.abort();
this.finished = true;

View File

@ -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<void> {
@ -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<any> {
if (this._isCancelled) {
waitForEvent(eventType: VerificationEventTypes): Promise<any> {
if (this.isCancelled) {
throw new VerificationCancelledError();
}
// Check if we already received the message

View File

@ -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(

View File

@ -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 = {

View File

@ -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);
});
}
}

View File

@ -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 };
}

View File

@ -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;
}

View File

@ -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);

View File

@ -62,11 +62,24 @@ export class KeyBackupSettingsView extends TemplateView<KeyBackupViewModel> {
return t.p("Cross-signing master key found and trusted.")
}),
t.if(vm => vm.canSignOwnDevice, t => {
return t.button({
onClick: disableTargetCallback(async () => {
return t.div([
t.button(
{
onClick: disableTargetCallback(async (evt) => {
await vm.signOwnDevice();
})
}, "Sign own device");
}),
},
"Sign own device"
),
t.button(
{
onClick: disableTargetCallback(async () => {
vm.navigateToVerification();
}),
},
"Verify by emoji"
),
]);
}),
]);

View File

@ -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<DeviceVerificationViewModel> {
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);
}
})
])
}
}

View File

@ -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<SelectMethodViewModel> {
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"
),
]),
]);
}),
]);
}
}

View File

@ -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<VerificationCancelledViewModel> {
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] ?? "";
}
}

View File

@ -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<VerificationCompleteViewModel> {
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")
]),
]);
}
}

View File

@ -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<VerifyEmojisViewModel> {
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`
),
]);
}
})
]);
}
}

View File

@ -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<WaitingForOtherUserViewModel> {
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")
),
]);
}
}