mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
Merge pull request #1057 from vector-im/device-verification-ui
Implement UI for device verification
This commit is contained in:
commit
02c9d6d9d4
@ -34,6 +34,8 @@ export type SegmentType = {
|
|||||||
"details": true;
|
"details": true;
|
||||||
"members": true;
|
"members": true;
|
||||||
"member": string;
|
"member": string;
|
||||||
|
"device-verification": string | boolean;
|
||||||
|
"join-room": true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createNavigation(): Navigation<SegmentType> {
|
export function createNavigation(): Navigation<SegmentType> {
|
||||||
@ -51,7 +53,7 @@ function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<Se
|
|||||||
// allowed root segments
|
// allowed root segments
|
||||||
return type === "login" || type === "session" || type === "sso" || type === "logout";
|
return type === "login" || type === "session" || type === "sso" || type === "logout";
|
||||||
case "session":
|
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":
|
case "rooms":
|
||||||
// downside of the approach: both of these will control which tile is selected
|
// downside of the approach: both of these will control which tile is selected
|
||||||
return type === "room" || type === "empty-grid-tile";
|
return type === "room" || type === "empty-grid-tile";
|
||||||
|
@ -26,6 +26,7 @@ import {RoomGridViewModel} from "./RoomGridViewModel.js";
|
|||||||
import {SettingsViewModel} from "./settings/SettingsViewModel.js";
|
import {SettingsViewModel} from "./settings/SettingsViewModel.js";
|
||||||
import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
|
import {CreateRoomViewModel} from "./CreateRoomViewModel.js";
|
||||||
import {JoinRoomViewModel} from "./JoinRoomViewModel";
|
import {JoinRoomViewModel} from "./JoinRoomViewModel";
|
||||||
|
import {DeviceVerificationViewModel} from "./verification/DeviceVerificationViewModel";
|
||||||
import {ViewModel} from "../ViewModel";
|
import {ViewModel} from "../ViewModel";
|
||||||
import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
|
import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
|
||||||
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";
|
import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js";
|
||||||
@ -48,6 +49,7 @@ export class SessionViewModel extends ViewModel {
|
|||||||
this._gridViewModel = null;
|
this._gridViewModel = null;
|
||||||
this._createRoomViewModel = null;
|
this._createRoomViewModel = null;
|
||||||
this._joinRoomViewModel = null;
|
this._joinRoomViewModel = null;
|
||||||
|
this._verificationViewModel = null;
|
||||||
this._toastCollectionViewModel = this.track(new ToastCollectionViewModel(this.childOptions({
|
this._toastCollectionViewModel = this.track(new ToastCollectionViewModel(this.childOptions({
|
||||||
session: this._client.session,
|
session: this._client.session,
|
||||||
})));
|
})));
|
||||||
@ -95,6 +97,14 @@ export class SessionViewModel extends ViewModel {
|
|||||||
}));
|
}));
|
||||||
this._updateJoinRoom(joinRoom.get());
|
this._updateJoinRoom(joinRoom.get());
|
||||||
|
|
||||||
|
if (this.features.crossSigning) {
|
||||||
|
const verification = this.navigation.observe("device-verification");
|
||||||
|
this.track(verification.subscribe((txnId) => {
|
||||||
|
this._updateVerification(txnId);
|
||||||
|
}));
|
||||||
|
this._updateVerification(verification.get());
|
||||||
|
}
|
||||||
|
|
||||||
const lightbox = this.navigation.observe("lightbox");
|
const lightbox = this.navigation.observe("lightbox");
|
||||||
this.track(lightbox.subscribe(eventId => {
|
this.track(lightbox.subscribe(eventId => {
|
||||||
this._updateLightbox(eventId);
|
this._updateLightbox(eventId);
|
||||||
@ -143,7 +153,8 @@ export class SessionViewModel extends ViewModel {
|
|||||||
this._gridViewModel ||
|
this._gridViewModel ||
|
||||||
this._settingsViewModel ||
|
this._settingsViewModel ||
|
||||||
this._createRoomViewModel ||
|
this._createRoomViewModel ||
|
||||||
this._joinRoomViewModel
|
this._joinRoomViewModel ||
|
||||||
|
this._verificationViewModel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +190,10 @@ export class SessionViewModel extends ViewModel {
|
|||||||
return this._joinRoomViewModel;
|
return this._joinRoomViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get verificationViewModel() {
|
||||||
|
return this._verificationViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
get toastCollectionViewModel() {
|
get toastCollectionViewModel() {
|
||||||
return this._toastCollectionViewModel;
|
return this._toastCollectionViewModel;
|
||||||
}
|
}
|
||||||
@ -327,6 +342,17 @@ export class SessionViewModel extends ViewModel {
|
|||||||
this.emitChange("activeMiddleViewModel");
|
this.emitChange("activeMiddleViewModel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateVerification(txnId) {
|
||||||
|
if (this._verificationViewModel) {
|
||||||
|
this._verificationViewModel = this.disposeTracked(this._verificationViewModel);
|
||||||
|
}
|
||||||
|
if (txnId) {
|
||||||
|
const request = this._client.session.crossSigning.get()?.receivedSASVerifications.get(txnId);
|
||||||
|
this._verificationViewModel = this.track(new DeviceVerificationViewModel(this.childOptions({ session: this._client.session, request })));
|
||||||
|
}
|
||||||
|
this.emitChange("activeMiddleViewModel");
|
||||||
|
}
|
||||||
|
|
||||||
_updateLightbox(eventId) {
|
_updateLightbox(eventId) {
|
||||||
if (this._lightboxViewModel) {
|
if (this._lightboxViewModel) {
|
||||||
this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel);
|
this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel);
|
||||||
|
@ -157,6 +157,10 @@ export class KeyBackupViewModel extends ViewModel<SegmentType, Options> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToVerification(): void {
|
||||||
|
this.navigation.push("device-verification", true);
|
||||||
|
}
|
||||||
|
|
||||||
get backupWriteStatus(): BackupWriteStatus {
|
get backupWriteStatus(): BackupWriteStatus {
|
||||||
const keyBackup = this._keyBackup;
|
const keyBackup = this._keyBackup;
|
||||||
if (!keyBackup || keyBackup.version === undefined) {
|
if (!keyBackup || keyBackup.version === undefined) {
|
||||||
|
@ -17,9 +17,11 @@ limitations under the License.
|
|||||||
import {ConcatList} from "../../../observable";
|
import {ConcatList} from "../../../observable";
|
||||||
import {ViewModel, Options as BaseOptions} from "../../ViewModel";
|
import {ViewModel, Options as BaseOptions} from "../../ViewModel";
|
||||||
import {CallToastCollectionViewModel} from "./calls/CallsToastCollectionViewModel";
|
import {CallToastCollectionViewModel} from "./calls/CallsToastCollectionViewModel";
|
||||||
|
import {VerificationToastCollectionViewModel} from "./verification/VerificationToastCollectionViewModel";
|
||||||
import type {Session} from "../../../matrix/Session.js";
|
import type {Session} from "../../../matrix/Session.js";
|
||||||
import type {SegmentType} from "../../navigation";
|
import type {SegmentType} from "../../navigation";
|
||||||
import type {BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel";
|
import type {BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel";
|
||||||
|
import type {IToastCollection} from "./IToastCollection";
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
session: Session;
|
session: Session;
|
||||||
@ -31,9 +33,16 @@ export class ToastCollectionViewModel extends ViewModel<SegmentType, Options> {
|
|||||||
constructor(options: Options) {
|
constructor(options: Options) {
|
||||||
super(options);
|
super(options);
|
||||||
const session = this.getOption("session");
|
const session = this.getOption("session");
|
||||||
const vms = [
|
const collectionVms: IToastCollection[] = [];
|
||||||
this.track(new CallToastCollectionViewModel(this.childOptions({ session }))),
|
if (this.features.calls) {
|
||||||
].map(vm => vm.toastViewModels);
|
collectionVms.push(this.track(new CallToastCollectionViewModel(this.childOptions({ session }))));
|
||||||
|
}
|
||||||
|
if (this.features.crossSigning) {
|
||||||
|
collectionVms.push(this.track(new VerificationToastCollectionViewModel(this.childOptions({ session }))));
|
||||||
|
}
|
||||||
|
const vms: IToastCollection["toastViewModels"][] = collectionVms.map(vm => vm.toastViewModels);
|
||||||
|
if (vms.length !== 0) {
|
||||||
this.toastViewModels = new ConcatList(...vms);
|
this.toastViewModels = new ConcatList(...vms);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
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 {VerificationToastNotificationViewModel} from "./VerificationToastNotificationViewModel";
|
||||||
|
import {ObservableArray} from "../../../../observable";
|
||||||
|
import {ViewModel, Options as BaseOptions} from "../../../ViewModel";
|
||||||
|
import type {Session} from "../../../../matrix/Session.js";
|
||||||
|
import type {SegmentType} from "../../../navigation";
|
||||||
|
import type {IToastCollection} from "../IToastCollection";
|
||||||
|
import type {SASRequest} from "../../../../matrix/verification/SAS/SASRequest";
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
session: Session;
|
||||||
|
} & BaseOptions;
|
||||||
|
|
||||||
|
export class VerificationToastCollectionViewModel extends ViewModel<SegmentType, Options> implements IToastCollection {
|
||||||
|
public readonly toastViewModels: ObservableArray<VerificationToastNotificationViewModel> = new ObservableArray();
|
||||||
|
|
||||||
|
constructor(options: Options) {
|
||||||
|
super(options);
|
||||||
|
this.subscribeToSASRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribeToSASRequests() {
|
||||||
|
await this.getOption("session").crossSigning.waitFor(v => !!v).promise;
|
||||||
|
const crossSigning = this.getOption("session").crossSigning.get();
|
||||||
|
this.track(crossSigning.receivedSASVerifications.subscribe(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async onAdd(_, request: SASRequest) {
|
||||||
|
const dismiss = () => {
|
||||||
|
const idx = this.toastViewModels.array.findIndex(vm => vm.request.id === request.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.toastViewModels.remove(idx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.toastViewModels.append(
|
||||||
|
this.track(new VerificationToastNotificationViewModel(this.childOptions({ request, dismiss })))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(_, request: SASRequest) {
|
||||||
|
const idx = this.toastViewModels.array.findIndex(vm => vm.request.id === request.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.toastViewModels.remove(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(_, request: SASRequest) {
|
||||||
|
const idx = this.toastViewModels.array.findIndex(vm => vm.request.id === request.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.toastViewModels.update(idx, this.toastViewModels.at(idx)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset() {
|
||||||
|
for (let i = 0; i < this.toastViewModels.length; ++i) {
|
||||||
|
this.toastViewModels.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
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 {BaseClassOptions, BaseToastNotificationViewModel} from ".././BaseToastNotificationViewModel";
|
||||||
|
import {SegmentType} from "../../../navigation";
|
||||||
|
import {SASRequest} from "../../../../matrix/verification/SAS/SASRequest";
|
||||||
|
|
||||||
|
type Options<N extends MinimumNeededSegmentType = SegmentType> = {
|
||||||
|
request: SASRequest;
|
||||||
|
} & BaseClassOptions<N>;
|
||||||
|
|
||||||
|
type MinimumNeededSegmentType = {
|
||||||
|
"device-verification": string | boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class VerificationToastNotificationViewModel<N extends MinimumNeededSegmentType = SegmentType, O extends Options<N> = Options<N>> extends BaseToastNotificationViewModel<N, O> {
|
||||||
|
constructor(options: O) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
get kind(): "verification" {
|
||||||
|
return "verification";
|
||||||
|
}
|
||||||
|
|
||||||
|
get request(): SASRequest {
|
||||||
|
return this.getOption("request");
|
||||||
|
}
|
||||||
|
|
||||||
|
get otherDeviceId(): string {
|
||||||
|
return this.request.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
accept() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.navigation.push("device-verification", this.request.id);
|
||||||
|
this.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
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";
|
||||||
|
import type {SASRequest} from "../../../matrix/verification/SAS/SASRequest";
|
||||||
|
|
||||||
|
type Options = BaseOptions & {
|
||||||
|
session: Session;
|
||||||
|
request: SASRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DeviceVerificationViewModel extends ErrorReportViewModel<SegmentType, Options> {
|
||||||
|
private sas: SASVerification;
|
||||||
|
private _currentStageViewModel: any;
|
||||||
|
|
||||||
|
constructor(options: Readonly<Options>) {
|
||||||
|
super(options);
|
||||||
|
const sasRequest = options.request;
|
||||||
|
if (options.request) {
|
||||||
|
this.start(sasRequest);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We are about to send the request
|
||||||
|
this.start(this.getOption("session").userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async start(requestOrUserId: SASRequest | string) {
|
||||||
|
await this.logAndCatch("DeviceVerificationViewModel.start", (log) => {
|
||||||
|
const crossSigning = this.getOption("session").crossSigning.get();
|
||||||
|
this.sas = crossSigning.startVerification(requestOrUserId, log);
|
||||||
|
this.addEventListeners();
|
||||||
|
if (typeof requestOrUserId === "string") {
|
||||||
|
this.updateCurrentStageViewModel(new WaitingForOtherUserViewModel(this.childOptions({ sas: this.sas })));
|
||||||
|
}
|
||||||
|
return this.sas.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEventListeners() {
|
||||||
|
this.track(this.sas.disposableOn("SelectVerificationStage", (stage) => {
|
||||||
|
this.updateCurrentStageViewModel(
|
||||||
|
new SelectMethodViewModel(this.childOptions({ sas: this.sas, stage: stage!, }))
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
this.track(this.sas.disposableOn("EmojiGenerated", (stage) => {
|
||||||
|
this.updateCurrentStageViewModel(
|
||||||
|
new VerifyEmojisViewModel(this.childOptions({ stage: stage!, }))
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
this.track(this.sas.disposableOn("VerificationCancelled", (cancellation) => {
|
||||||
|
this.updateCurrentStageViewModel(
|
||||||
|
new VerificationCancelledViewModel(
|
||||||
|
this.childOptions({ cancellation: cancellation! })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
this.track(this.sas.disposableOn("VerificationCompleted", (deviceId) => {
|
||||||
|
this.updateCurrentStageViewModel(
|
||||||
|
new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! }))
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCurrentStageViewModel(vm) {
|
||||||
|
this._currentStageViewModel = this.disposeTracked(this._currentStageViewModel);
|
||||||
|
this._currentStageViewModel = this.track(vm);
|
||||||
|
this.emitChange("currentStageViewModel");
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentStageViewModel() {
|
||||||
|
return this._currentStageViewModel;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
get kind(): string {
|
||||||
|
return "select-method";
|
||||||
|
}
|
||||||
|
}
|
@ -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 {ViewModel, Options as BaseOptions} from "../../../ViewModel";
|
||||||
|
import {SegmentType} from "../../../navigation/index";
|
||||||
|
import type {CancelReason} from "../../../../matrix/verification/SAS/channel/types";
|
||||||
|
import type {IChannel} from "../../../../matrix/verification/SAS/channel/Channel";
|
||||||
|
|
||||||
|
type Options = BaseOptions & {
|
||||||
|
cancellation: IChannel["cancellation"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class VerificationCancelledViewModel extends ViewModel<SegmentType, Options> {
|
||||||
|
get cancelCode(): CancelReason {
|
||||||
|
return this.options.cancellation!.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCancelledByUs(): boolean {
|
||||||
|
return this.options.cancellation!.cancelledByUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
gotoSettings() {
|
||||||
|
this.navigation.push("settings", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get kind(): string {
|
||||||
|
return "verification-cancelled";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
get kind(): string {
|
||||||
|
return "verification-completed";
|
||||||
|
}
|
||||||
|
}
|
@ -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 {CalculateSASStage} from "../../../../matrix/verification/SAS/stages/CalculateSASStage";
|
||||||
|
|
||||||
|
type Options = BaseOptions & {
|
||||||
|
stage: CalculateSASStage;
|
||||||
|
session: Session;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class VerifyEmojisViewModel extends ErrorReportViewModel<SegmentType, Options> {
|
||||||
|
private _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;
|
||||||
|
}
|
||||||
|
|
||||||
|
get kind(): string {
|
||||||
|
return "verify-emojis";
|
||||||
|
}
|
||||||
|
|
||||||
|
get isWaiting(): boolean {
|
||||||
|
return this._isWaiting;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
get kind(): string {
|
||||||
|
return "waiting-for-user";
|
||||||
|
}
|
||||||
|
}
|
@ -365,9 +365,11 @@ export class Session {
|
|||||||
olm: this._olm,
|
olm: this._olm,
|
||||||
olmUtil: this._olmUtil,
|
olmUtil: this._olmUtil,
|
||||||
deviceTracker: this._deviceTracker,
|
deviceTracker: this._deviceTracker,
|
||||||
|
deviceMessageHandler: this._deviceMessageHandler,
|
||||||
hsApi: this._hsApi,
|
hsApi: this._hsApi,
|
||||||
ownUserId: this.userId,
|
ownUserId: this.userId,
|
||||||
e2eeAccount: this._e2eeAccount
|
e2eeAccount: this._e2eeAccount,
|
||||||
|
deviceId: this.deviceId,
|
||||||
});
|
});
|
||||||
if (await crossSigning.load(log)) {
|
if (await crossSigning.load(log)) {
|
||||||
this._crossSigning.set(crossSigning);
|
this._crossSigning.set(crossSigning);
|
||||||
|
@ -19,6 +19,8 @@ import {pkSign} from "./common";
|
|||||||
import {SASVerification} from "./SAS/SASVerification";
|
import {SASVerification} from "./SAS/SASVerification";
|
||||||
import {ToDeviceChannel} from "./SAS/channel/Channel";
|
import {ToDeviceChannel} from "./SAS/channel/Channel";
|
||||||
import {VerificationEventType} from "./SAS/channel/types";
|
import {VerificationEventType} from "./SAS/channel/types";
|
||||||
|
import {ObservableMap} from "../../observable/map";
|
||||||
|
import {SASRequest} from "./SAS/SASRequest";
|
||||||
import type {SecretStorage} from "../ssss/SecretStorage";
|
import type {SecretStorage} from "../ssss/SecretStorage";
|
||||||
import type {Storage} from "../storage/idb/Storage";
|
import type {Storage} from "../storage/idb/Storage";
|
||||||
import type {Platform} from "../../platform/web/Platform";
|
import type {Platform} from "../../platform/web/Platform";
|
||||||
@ -29,6 +31,7 @@ import type {ILogItem} from "../../logging/types";
|
|||||||
import type {DeviceMessageHandler} from "../DeviceMessageHandler.js";
|
import type {DeviceMessageHandler} from "../DeviceMessageHandler.js";
|
||||||
import type {SignedValue, DeviceKey} from "../e2ee/common";
|
import type {SignedValue, DeviceKey} from "../e2ee/common";
|
||||||
import type * as OlmNamespace from "@matrix-org/olm";
|
import type * as OlmNamespace from "@matrix-org/olm";
|
||||||
|
|
||||||
type Olm = typeof OlmNamespace;
|
type Olm = typeof OlmNamespace;
|
||||||
|
|
||||||
// we store cross-signing (and device) keys in the format we get them from the server
|
// we store cross-signing (and device) keys in the format we get them from the server
|
||||||
@ -88,6 +91,7 @@ export class CrossSigning {
|
|||||||
private _isMasterKeyTrusted: boolean = false;
|
private _isMasterKeyTrusted: boolean = false;
|
||||||
private readonly deviceId: string;
|
private readonly deviceId: string;
|
||||||
private sasVerificationInProgress?: SASVerification;
|
private sasVerificationInProgress?: SASVerification;
|
||||||
|
public receivedSASVerifications: ObservableMap<string, SASRequest> = new ObservableMap();
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
storage: Storage,
|
storage: Storage,
|
||||||
@ -115,21 +119,7 @@ export class CrossSigning {
|
|||||||
this.deviceMessageHandler = options.deviceMessageHandler;
|
this.deviceMessageHandler = options.deviceMessageHandler;
|
||||||
|
|
||||||
this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => {
|
this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => {
|
||||||
if (this.sasVerificationInProgress &&
|
this._handleSASDeviceMessage(unencryptedEvent);
|
||||||
(
|
|
||||||
!this.sasVerificationInProgress.finished ||
|
|
||||||
// If the start message is for the previous sasverification, ignore it.
|
|
||||||
this.sasVerificationInProgress.channel.id === unencryptedEvent.content.transaction_id
|
|
||||||
)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (unencryptedEvent.type === VerificationEventType.Request ||
|
|
||||||
unencryptedEvent.type === VerificationEventType.Start) {
|
|
||||||
await this.platform.logger.run("Start verification from request", async (log) => {
|
|
||||||
const sas = this.startVerification(unencryptedEvent.sender, unencryptedEvent, log);
|
|
||||||
await sas?.start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,14 +172,18 @@ export class CrossSigning {
|
|||||||
return this._isMasterKeyTrusted;
|
return this._isMasterKeyTrusted;
|
||||||
}
|
}
|
||||||
|
|
||||||
startVerification(userId: string, startingMessage: any, log: ILogItem): SASVerification | undefined {
|
startVerification(requestOrUserId: SASRequest, log: ILogItem): SASVerification | undefined;
|
||||||
|
startVerification(requestOrUserId: string, log: ILogItem): SASVerification | undefined;
|
||||||
|
startVerification(requestOrUserId: string | SASRequest, log: ILogItem): SASVerification | undefined {
|
||||||
if (this.sasVerificationInProgress && !this.sasVerificationInProgress.finished) {
|
if (this.sasVerificationInProgress && !this.sasVerificationInProgress.finished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const otherUserId = requestOrUserId instanceof SASRequest ? requestOrUserId.sender : requestOrUserId;
|
||||||
|
const startingMessage = requestOrUserId instanceof SASRequest ? requestOrUserId.startingMessage : undefined;
|
||||||
const channel = new ToDeviceChannel({
|
const channel = new ToDeviceChannel({
|
||||||
deviceTracker: this.deviceTracker,
|
deviceTracker: this.deviceTracker,
|
||||||
hsApi: this.hsApi,
|
hsApi: this.hsApi,
|
||||||
otherUserId: userId,
|
otherUserId,
|
||||||
clock: this.platform.clock,
|
clock: this.platform.clock,
|
||||||
deviceMessageHandler: this.deviceMessageHandler,
|
deviceMessageHandler: this.deviceMessageHandler,
|
||||||
ourUserDeviceId: this.deviceId,
|
ourUserDeviceId: this.deviceId,
|
||||||
@ -201,7 +195,7 @@ export class CrossSigning {
|
|||||||
olmUtil: this.olmUtil,
|
olmUtil: this.olmUtil,
|
||||||
ourUserId: this.ownUserId,
|
ourUserId: this.ownUserId,
|
||||||
ourUserDeviceId: this.deviceId,
|
ourUserDeviceId: this.deviceId,
|
||||||
otherUserId: userId,
|
otherUserId,
|
||||||
log,
|
log,
|
||||||
channel,
|
channel,
|
||||||
e2eeAccount: this.e2eeAccount,
|
e2eeAccount: this.e2eeAccount,
|
||||||
@ -212,6 +206,35 @@ export class CrossSigning {
|
|||||||
return this.sasVerificationInProgress;
|
return this.sasVerificationInProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleSASDeviceMessage(event: any) {
|
||||||
|
const txnId = event.content.transaction_id;
|
||||||
|
/**
|
||||||
|
* If we receive an event for the current/previously finished
|
||||||
|
* SAS verification, we should ignore it because the device channel
|
||||||
|
* object (who also listens for to_device messages) will take care of it (if needed).
|
||||||
|
*/
|
||||||
|
const shouldIgnoreEvent = this.sasVerificationInProgress?.channel.id === txnId;
|
||||||
|
if (shouldIgnoreEvent) { return; }
|
||||||
|
/**
|
||||||
|
* 1. If we receive the cancel message, we need to update the requests map.
|
||||||
|
* 2. If we receive an starting message (viz request/start), we need to create the SASRequest from it.
|
||||||
|
*/
|
||||||
|
switch (event.type) {
|
||||||
|
case VerificationEventType.Cancel:
|
||||||
|
this.receivedSASVerifications.remove(txnId);
|
||||||
|
return;
|
||||||
|
case VerificationEventType.Request:
|
||||||
|
case VerificationEventType.Start:
|
||||||
|
this.platform.logger.run("Create SASRequest", () => {
|
||||||
|
this.receivedSASVerifications.set(txnId, new SASRequest(event));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
// we don't care about this event!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** returns our own device key signed by our self-signing key. Other signatures will be missing. */
|
/** returns our own device key signed by our self-signing key. Other signatures will be missing. */
|
||||||
async signOwnDevice(log: ILogItem): Promise<DeviceKey | undefined> {
|
async signOwnDevice(log: ILogItem): Promise<DeviceKey | undefined> {
|
||||||
return log.wrap("CrossSigning.signOwnDevice", async log => {
|
return log.wrap("CrossSigning.signOwnDevice", async log => {
|
||||||
|
31
src/matrix/verification/SAS/SASRequest.ts
Normal file
31
src/matrix/verification/SAS/SASRequest.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SASRequest {
|
||||||
|
constructor(public readonly startingMessage: any) {}
|
||||||
|
|
||||||
|
get deviceId(): string {
|
||||||
|
return this.startingMessage.content.from_device;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sender(): string {
|
||||||
|
return this.startingMessage.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id(): string {
|
||||||
|
return this.startingMessage.content.transaction_id;
|
||||||
|
}
|
||||||
|
}
|
@ -19,14 +19,14 @@ import type {BaseSASVerificationStage} from "./stages/BaseSASVerificationStage";
|
|||||||
import type {Account} from "../../e2ee/Account.js";
|
import type {Account} from "../../e2ee/Account.js";
|
||||||
import type {DeviceTracker} from "../../e2ee/DeviceTracker.js";
|
import type {DeviceTracker} from "../../e2ee/DeviceTracker.js";
|
||||||
import type * as OlmNamespace from "@matrix-org/olm";
|
import type * as OlmNamespace from "@matrix-org/olm";
|
||||||
import {IChannel} from "./channel/Channel";
|
import type {IChannel} from "./channel/Channel";
|
||||||
import {HomeServerApi} from "../../net/HomeServerApi";
|
import type {HomeServerApi} from "../../net/HomeServerApi";
|
||||||
|
import type {Timeout} from "../../../platform/types/types";
|
||||||
|
import type {Clock} from "../../../platform/web/dom/Clock.js";
|
||||||
import {CancelReason, VerificationEventType} from "./channel/types";
|
import {CancelReason, VerificationEventType} from "./channel/types";
|
||||||
import {SendReadyStage} from "./stages/SendReadyStage";
|
import {SendReadyStage} from "./stages/SendReadyStage";
|
||||||
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||||
import {VerificationCancelledError} from "./VerificationCancelledError";
|
import {VerificationCancelledError} from "./VerificationCancelledError";
|
||||||
import {Timeout} from "../../../platform/types/types";
|
|
||||||
import {Clock} from "../../../platform/web/dom/Clock.js";
|
|
||||||
import {EventEmitter} from "../../../utils/EventEmitter";
|
import {EventEmitter} from "../../../utils/EventEmitter";
|
||||||
import {SASProgressEvents} from "./types";
|
import {SASProgressEvents} from "./types";
|
||||||
|
|
||||||
@ -84,6 +84,10 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async abort() {
|
||||||
|
await this.channel.cancelVerification(CancelReason.UserCancelled);
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
try {
|
try {
|
||||||
let stage = this.startStage;
|
let stage = this.startStage;
|
||||||
@ -98,6 +102,9 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
if (this.channel.isCancelled) {
|
||||||
|
this.emit("VerificationCancelled", this.channel.cancellation);
|
||||||
|
}
|
||||||
this.olmSas.free();
|
this.olmSas.free();
|
||||||
this.timeout.abort();
|
this.timeout.abort();
|
||||||
this.finished = true;
|
this.finished = true;
|
||||||
@ -163,6 +170,9 @@ export function tests() {
|
|||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
keys: {
|
keys: {
|
||||||
[`ed25519:${deviceId}`]: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q",
|
[`ed25519:${deviceId}`]: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q",
|
||||||
|
},
|
||||||
|
unsigned: {
|
||||||
|
device_display_name: "lala10",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -49,6 +49,8 @@ export interface IChannel {
|
|||||||
acceptMessage: any;
|
acceptMessage: any;
|
||||||
startMessage: any;
|
startMessage: any;
|
||||||
initiatedByUs: boolean;
|
initiatedByUs: boolean;
|
||||||
|
isCancelled: boolean;
|
||||||
|
cancellation?: { code: CancelReason, cancelledByUs: boolean };
|
||||||
id: string;
|
id: string;
|
||||||
otherUserDeviceId: string;
|
otherUserDeviceId: string;
|
||||||
}
|
}
|
||||||
@ -78,7 +80,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
|||||||
public startMessage: any;
|
public startMessage: any;
|
||||||
public id: string;
|
public id: string;
|
||||||
private _initiatedByUs: boolean;
|
private _initiatedByUs: boolean;
|
||||||
private _isCancelled = false;
|
private _cancellation?: { code: CancelReason, cancelledByUs: boolean };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -116,8 +118,12 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get cancellation(): IChannel["cancellation"] {
|
||||||
|
return this._cancellation;
|
||||||
|
};
|
||||||
|
|
||||||
get isCancelled(): boolean {
|
get isCancelled(): boolean {
|
||||||
return this._isCancelled;
|
return !!this._cancellation;
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void> {
|
async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void> {
|
||||||
@ -199,7 +205,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.type === VerificationEventType.Cancel) {
|
if (event.type === VerificationEventType.Cancel) {
|
||||||
this._isCancelled = true;
|
this._cancellation = { code: event.content.code, cancelledByUs: false };
|
||||||
this.dispose();
|
this.dispose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -234,7 +240,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
|||||||
const payload = {
|
const payload = {
|
||||||
messages: {
|
messages: {
|
||||||
[this.otherUserId]: {
|
[this.otherUserId]: {
|
||||||
[this.otherUserDeviceId]: {
|
[this.otherUserDeviceId ?? "*"]: {
|
||||||
code: cancellationType,
|
code: cancellationType,
|
||||||
reason: messageFromErrorType[cancellationType],
|
reason: messageFromErrorType[cancellationType],
|
||||||
transaction_id: this.id,
|
transaction_id: this.id,
|
||||||
@ -243,7 +249,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response();
|
await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response();
|
||||||
this._isCancelled = true;
|
this._cancellation = { code: cancellationType, cancelledByUs: true };
|
||||||
this.dispose();
|
this.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -258,7 +264,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
waitForEvent(eventType: VerificationEventType): Promise<any> {
|
waitForEvent(eventType: VerificationEventType): Promise<any> {
|
||||||
if (this._isCancelled) {
|
if (this.isCancelled) {
|
||||||
throw new VerificationCancelledError();
|
throw new VerificationCancelledError();
|
||||||
}
|
}
|
||||||
// Check if we already received the message
|
// Check if we already received the message
|
||||||
|
@ -17,6 +17,7 @@ export class MockChannel implements ITestChannel {
|
|||||||
public initiatedByUs: boolean;
|
public initiatedByUs: boolean;
|
||||||
public startMessage: any;
|
public startMessage: any;
|
||||||
public isCancelled: boolean = false;
|
public isCancelled: boolean = false;
|
||||||
|
public cancellation: { code: CancelReason; cancelledByUs: boolean; };
|
||||||
private olmSas: any;
|
private olmSas: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -23,9 +23,11 @@ import type {ILogItem} from "../../../../logging/types";
|
|||||||
export class SelectVerificationMethodStage extends BaseSASVerificationStage {
|
export class SelectVerificationMethodStage extends BaseSASVerificationStage {
|
||||||
private hasSentStartMessage = false;
|
private hasSentStartMessage = false;
|
||||||
private allowSelection = true;
|
private allowSelection = true;
|
||||||
|
public otherDeviceName: string;
|
||||||
|
|
||||||
async completeStage() {
|
async completeStage() {
|
||||||
await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => {
|
await this.log.wrap("SelectVerificationMethodStage.completeStage", async (log) => {
|
||||||
|
await this.findDeviceName(log);
|
||||||
this.eventEmitter.emit("SelectVerificationStage", this);
|
this.eventEmitter.emit("SelectVerificationStage", this);
|
||||||
const startMessage = this.channel.waitForEvent(VerificationEventType.Start);
|
const startMessage = this.channel.waitForEvent(VerificationEventType.Start);
|
||||||
const acceptMessage = this.channel.waitForEvent(VerificationEventType.Accept);
|
const acceptMessage = this.channel.waitForEvent(VerificationEventType.Accept);
|
||||||
@ -81,6 +83,17 @@ 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);
|
||||||
|
if (!device) {
|
||||||
|
log.log({ l: "Cannot find device", userId: this.otherUserId, deviceId: this.otherUserDeviceId });
|
||||||
|
throw new Error("Cannot find device");
|
||||||
|
}
|
||||||
|
this.otherDeviceName = device.unsigned.device_display_name ?? device.device_id;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async selectEmojiMethod(log: ILogItem) {
|
async selectEmojiMethod(log: ILogItem) {
|
||||||
if (!this.allowSelection) { return; }
|
if (!this.allowSelection) { return; }
|
||||||
const content = {
|
const content = {
|
||||||
|
@ -19,6 +19,7 @@ import {VerificationEventType} from "../channel/types";
|
|||||||
export class SendDoneStage extends BaseSASVerificationStage {
|
export class SendDoneStage extends BaseSASVerificationStage {
|
||||||
async completeStage() {
|
async completeStage() {
|
||||||
await this.log.wrap("SendDoneStage.completeStage", async (log) => {
|
await this.log.wrap("SendDoneStage.completeStage", async (log) => {
|
||||||
|
this.eventEmitter.emit("VerificationCompleted", this.otherUserDeviceId);
|
||||||
await this.channel.send(VerificationEventType.Done, {}, log);
|
await this.channel.send(VerificationEventType.Done, {}, log);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export class SendMacStage extends BaseSASVerificationStage {
|
|||||||
this.channel.id;
|
this.channel.id;
|
||||||
|
|
||||||
const deviceKeyId = `ed25519:${this.ourUserDeviceId}`;
|
const deviceKeyId = `ed25519:${this.ourUserDeviceId}`;
|
||||||
const deviceKeys = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning();
|
const deviceKeys = this.e2eeAccount.getUnsignedDeviceKey();
|
||||||
mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId);
|
mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + deviceKeyId);
|
||||||
keyList.push(deviceKeyId);
|
keyList.push(deviceKeyId);
|
||||||
|
|
||||||
|
@ -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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import {CalculateSASStage} from "./stages/CalculateSASStage";
|
import type {IChannel} from "./channel/Channel";
|
||||||
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
import type {CalculateSASStage} from "./stages/CalculateSASStage";
|
||||||
|
import type {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||||
|
|
||||||
export type SASProgressEvents = {
|
export type SASProgressEvents = {
|
||||||
SelectVerificationStage: SelectVerificationMethodStage;
|
SelectVerificationStage: SelectVerificationMethodStage;
|
||||||
EmojiGenerated: CalculateSASStage;
|
EmojiGenerated: CalculateSASStage;
|
||||||
|
VerificationCompleted: string;
|
||||||
|
VerificationCancelled: IChannel["cancellation"];
|
||||||
}
|
}
|
||||||
|
@ -1264,27 +1264,52 @@ button.RoomDetailsView_row::after {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView:not(:first-child),
|
||||||
.CallToastNotificationView:not(:first-child) {
|
.CallToastNotificationView:not(:first-child) {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.CallToastNotificationView {
|
.CallToastNotificationView {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 40px 1fr 1fr 48px;
|
grid-template-rows: 40px 1fr 1fr 48px;
|
||||||
row-gap: 4px;
|
row-gap: 4px;
|
||||||
width: 260px;
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.VerificationToastNotificationView,
|
||||||
|
.CallToastNotificationView {
|
||||||
background-color: var(--background-color-secondary);
|
background-color: var(--background-color-secondary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CallToastNotificationView {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView {
|
||||||
|
width: 248px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView__top {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.CallToastNotificationView__top {
|
.CallToastNotificationView__top {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 176px auto;
|
grid-template-columns: auto 176px auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView__dismiss-btn,
|
||||||
.CallToastNotificationView__dismiss-btn {
|
.CallToastNotificationView__dismiss-btn {
|
||||||
background: center var(--background-color-secondary--darker-5) url("./icons/dismiss.svg?primary=text-color") no-repeat;
|
background: center var(--background-color-secondary--darker-5) url("./icons/dismiss.svg?primary=text-color") no-repeat;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
@ -1292,11 +1317,16 @@ button.RoomDetailsView_row::after {
|
|||||||
width: 15px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView__title,
|
||||||
.CallToastNotificationView__name {
|
.CallToastNotificationView__name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView__description {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.CallToastNotificationView__description {
|
.CallToastNotificationView__description {
|
||||||
margin-left: 42px;
|
margin-left: 42px;
|
||||||
}
|
}
|
||||||
@ -1350,7 +1380,105 @@ button.RoomDetailsView_row::after {
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView__action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.CallToastNotificationView__action .button-action {
|
.CallToastNotificationView__action .button-action {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VerificationToastNotificationView__action .button-action {
|
||||||
|
width: 100px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
@ -30,6 +30,7 @@ import {CreateRoomView} from "./CreateRoomView.js";
|
|||||||
import {RightPanelView} from "./rightpanel/RightPanelView.js";
|
import {RightPanelView} from "./rightpanel/RightPanelView.js";
|
||||||
import {viewClassForTile} from "./room/common";
|
import {viewClassForTile} from "./room/common";
|
||||||
import {JoinRoomView} from "./JoinRoomView";
|
import {JoinRoomView} from "./JoinRoomView";
|
||||||
|
import {DeviceVerificationView} from "./verification/DeviceVerificationView";
|
||||||
import {ToastCollectionView} from "./toast/ToastCollectionView";
|
import {ToastCollectionView} from "./toast/ToastCollectionView";
|
||||||
|
|
||||||
export class SessionView extends TemplateView {
|
export class SessionView extends TemplateView {
|
||||||
@ -53,6 +54,8 @@ export class SessionView extends TemplateView {
|
|||||||
return new CreateRoomView(vm.createRoomViewModel);
|
return new CreateRoomView(vm.createRoomViewModel);
|
||||||
} else if (vm.joinRoomViewModel) {
|
} else if (vm.joinRoomViewModel) {
|
||||||
return new JoinRoomView(vm.joinRoomViewModel);
|
return new JoinRoomView(vm.joinRoomViewModel);
|
||||||
|
} else if (vm.verificationViewModel) {
|
||||||
|
return new DeviceVerificationView(vm.verificationViewModel);
|
||||||
} else if (vm.currentRoomViewModel) {
|
} else if (vm.currentRoomViewModel) {
|
||||||
if (vm.currentRoomViewModel.kind === "invite") {
|
if (vm.currentRoomViewModel.kind === "invite") {
|
||||||
return new InviteView(vm.currentRoomViewModel);
|
return new InviteView(vm.currentRoomViewModel);
|
||||||
|
@ -62,11 +62,24 @@ export class KeyBackupSettingsView extends TemplateView<KeyBackupViewModel> {
|
|||||||
return t.p("Cross-signing master key found and trusted.")
|
return t.p("Cross-signing master key found and trusted.")
|
||||||
}),
|
}),
|
||||||
t.if(vm => vm.canSignOwnDevice, t => {
|
t.if(vm => vm.canSignOwnDevice, t => {
|
||||||
return t.button({
|
return t.div([
|
||||||
onClick: disableTargetCallback(async () => {
|
t.button(
|
||||||
|
{
|
||||||
|
onClick: disableTargetCallback(async (evt) => {
|
||||||
await vm.signOwnDevice();
|
await vm.signOwnDevice();
|
||||||
})
|
}),
|
||||||
}, "Sign own device");
|
},
|
||||||
|
"Sign own device"
|
||||||
|
),
|
||||||
|
t.button(
|
||||||
|
{
|
||||||
|
onClick: disableTargetCallback(async () => {
|
||||||
|
vm.navigateToVerification();
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"Verify by emoji"
|
||||||
|
),
|
||||||
|
]);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
@ -15,17 +15,21 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {CallToastNotificationView} from "./CallToastNotificationView";
|
import {CallToastNotificationView} from "./CallToastNotificationView";
|
||||||
|
import {VerificationToastNotificationView} from "./VerificationToastNotificationView";
|
||||||
import {ListView} from "../../general/ListView";
|
import {ListView} from "../../general/ListView";
|
||||||
import {TemplateView, Builder} from "../../general/TemplateView";
|
import {TemplateView, Builder} from "../../general/TemplateView";
|
||||||
import type {IView} from "../../general/types";
|
import type {IView} from "../../general/types";
|
||||||
import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/calls/CallToastNotificationViewModel";
|
import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/calls/CallToastNotificationViewModel";
|
||||||
import type {ToastCollectionViewModel} from "../../../../../domain/session/toast/ToastCollectionViewModel";
|
import type {ToastCollectionViewModel} from "../../../../../domain/session/toast/ToastCollectionViewModel";
|
||||||
import type {BaseToastNotificationViewModel} from "../../../../../domain/session/toast/BaseToastNotificationViewModel";
|
import type {BaseToastNotificationViewModel} from "../../../../../domain/session/toast/BaseToastNotificationViewModel";
|
||||||
|
import type {VerificationToastNotificationViewModel} from "../../../../../domain/session/toast/verification/VerificationToastNotificationViewModel";
|
||||||
|
|
||||||
function toastViewModelToView(vm: BaseToastNotificationViewModel): IView {
|
function toastViewModelToView(vm: BaseToastNotificationViewModel): IView {
|
||||||
switch (vm.kind) {
|
switch (vm.kind) {
|
||||||
case "calls":
|
case "calls":
|
||||||
return new CallToastNotificationView(vm as CallToastNotificationViewModel);
|
return new CallToastNotificationView(vm as CallToastNotificationViewModel);
|
||||||
|
case "verification":
|
||||||
|
return new VerificationToastNotificationView(vm as VerificationToastNotificationViewModel);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Cannot find view class for notification kind ${vm.kind}`);
|
throw new Error(`Cannot find view class for notification kind ${vm.kind}`);
|
||||||
}
|
}
|
||||||
@ -33,12 +37,13 @@ function toastViewModelToView(vm: BaseToastNotificationViewModel): IView {
|
|||||||
|
|
||||||
export class ToastCollectionView extends TemplateView<ToastCollectionViewModel> {
|
export class ToastCollectionView extends TemplateView<ToastCollectionViewModel> {
|
||||||
render(t: Builder<ToastCollectionViewModel>, vm: ToastCollectionViewModel) {
|
render(t: Builder<ToastCollectionViewModel>, vm: ToastCollectionViewModel) {
|
||||||
const view = new ListView({
|
return t.div({ className: "ToastCollectionView" }, [
|
||||||
|
t.ifView(vm => !!vm.toastViewModels, t => {
|
||||||
|
return new ListView({
|
||||||
list: vm.toastViewModels,
|
list: vm.toastViewModels,
|
||||||
parentProvidesUpdates: false,
|
parentProvidesUpdates: false,
|
||||||
}, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm));
|
}, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm));
|
||||||
return t.div({ className: "ToastCollectionView" }, [
|
}),
|
||||||
t.view(view),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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, Builder} from "../../general/TemplateView";
|
||||||
|
import type {VerificationToastNotificationViewModel} from "../../../../../domain/session/toast/verification/VerificationToastNotificationViewModel";
|
||||||
|
|
||||||
|
export class VerificationToastNotificationView extends TemplateView<VerificationToastNotificationViewModel> {
|
||||||
|
render(t: Builder<VerificationToastNotificationViewModel>, vm: VerificationToastNotificationViewModel) {
|
||||||
|
return t.div({ className: "VerificationToastNotificationView" }, [
|
||||||
|
t.div({ className: "VerificationToastNotificationView__top" }, [
|
||||||
|
t.span({ className: "VerificationToastNotificationView__title" },
|
||||||
|
vm.i18n`Device Verification`),
|
||||||
|
t.button({
|
||||||
|
className: "button-action VerificationToastNotificationView__dismiss-btn",
|
||||||
|
onClick: () => vm.dismiss(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
t.div({ className: "VerificationToastNotificationView__description" }, [
|
||||||
|
t.span(vm.i18n`Do you want to verify device ${vm.otherDeviceId}?`),
|
||||||
|
]),
|
||||||
|
t.div({ className: "VerificationToastNotificationView__action" }, [
|
||||||
|
t.button({
|
||||||
|
className: "button-action primary destructive",
|
||||||
|
onClick: () => vm.dismiss(),
|
||||||
|
}, vm.i18n`Ignore`),
|
||||||
|
t.button({
|
||||||
|
className: "button-action primary",
|
||||||
|
onClick: () => vm.accept(),
|
||||||
|
}, vm.i18n`Accept`),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
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 {Builder, TemplateView} from "../../general/TemplateView";
|
||||||
|
import {DeviceVerificationViewModel} from "../../../../../domain/session/verification/DeviceVerificationViewModel";
|
||||||
|
import {WaitingForOtherUserView} from "./stages/WaitingForOtherUserView";
|
||||||
|
import {VerificationCancelledView} from "./stages/VerificationCancelledView";
|
||||||
|
import {SelectMethodView} from "./stages/SelectMethodView";
|
||||||
|
import {VerifyEmojisView} from "./stages/VerifyEmojisView";
|
||||||
|
import {VerificationCompleteView} from "./stages/VerificationCompleteView";
|
||||||
|
|
||||||
|
export class DeviceVerificationView extends TemplateView<DeviceVerificationViewModel> {
|
||||||
|
render(t: Builder<DeviceVerificationViewModel>) {
|
||||||
|
return t.div({
|
||||||
|
className: {
|
||||||
|
"middle": true,
|
||||||
|
"DeviceVerificationView": true,
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
t.mapView(vm => vm.currentStageViewModel, (vm) => {
|
||||||
|
switch (vm?.kind) {
|
||||||
|
case "waiting-for-user": return new WaitingForOtherUserView(vm);
|
||||||
|
case "verification-cancelled": return new VerificationCancelledView(vm);
|
||||||
|
case "select-method": return new SelectMethodView(vm);
|
||||||
|
case "verify-emojis": return new VerifyEmojisView(vm);
|
||||||
|
case "verification-completed": return new VerificationCompleteView(vm);
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
@ -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 {Builder, 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: Builder<SelectMethodViewModel>) {
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
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 {Builder, TemplateView} from "../../../general/TemplateView";
|
||||||
|
import {VerificationCancelledViewModel} from "../../../../../../domain/session/verification/stages/VerificationCancelledViewModel";
|
||||||
|
import {CancelReason} from "../../../../../../matrix/verification/SAS/channel/types";
|
||||||
|
|
||||||
|
export class VerificationCancelledView extends TemplateView<VerificationCancelledViewModel> {
|
||||||
|
render(t: Builder<VerificationCancelledViewModel>, 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: CancelReason, isCancelledByUs: boolean): string {
|
||||||
|
const descriptionsWhenWeCancelled = {
|
||||||
|
[CancelReason.InvalidMessage]: "You other device sent an invalid message.",
|
||||||
|
[CancelReason.KeyMismatch]: "The key could not be verified.",
|
||||||
|
[CancelReason.TimedOut]: "The verification process timed out.",
|
||||||
|
[CancelReason.UnexpectedMessage]: "Your other device sent an unexpected message.",
|
||||||
|
[CancelReason.UnknownMethod]: "Your other device is using an unknown method for verification.",
|
||||||
|
[CancelReason.UnknownTransaction]: "Your other device sent a message with an unknown transaction id.",
|
||||||
|
[CancelReason.UserMismatch]: "The expected user did not match the user verified.",
|
||||||
|
[CancelReason.MismatchedCommitment]: "The hash commitment does not match.",
|
||||||
|
[CancelReason.MismatchedSAS]: "The emoji/decimal did not match.",
|
||||||
|
}
|
||||||
|
const descriptionsWhenTheyCancelled = {
|
||||||
|
[CancelReason.UserCancelled]: "Your other device cancelled the verification!",
|
||||||
|
[CancelReason.InvalidMessage]: "Invalid message sent to the other device.",
|
||||||
|
[CancelReason.KeyMismatch]: "The other device could not verify our keys",
|
||||||
|
[CancelReason.TimedOut]: "The verification process timed out.",
|
||||||
|
[CancelReason.UnexpectedMessage]: "Unexpected message sent to the other device.",
|
||||||
|
[CancelReason.UnknownMethod]: "Your other device does not understand the method you chose",
|
||||||
|
[CancelReason.UnknownTransaction]: "Your other device rejected our message.",
|
||||||
|
[CancelReason.UserMismatch]: "The expected user did not match the user verified.",
|
||||||
|
[CancelReason.MismatchedCommitment]: "Your other device was not able to verify the hash commitment",
|
||||||
|
[CancelReason.MismatchedSAS]: "The emoji/decimal did not match.",
|
||||||
|
}
|
||||||
|
const map = isCancelledByUs ? descriptionsWhenWeCancelled : descriptionsWhenTheyCancelled;
|
||||||
|
return map[code] ?? "";
|
||||||
|
}
|
||||||
|
}
|
@ -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 {Builder, TemplateView} from "../../../general/TemplateView";
|
||||||
|
import type {VerificationCompleteViewModel} from "../../../../../../domain/session/verification/stages/VerificationCompleteViewModel";
|
||||||
|
|
||||||
|
export class VerificationCompleteView extends TemplateView<VerificationCompleteViewModel> {
|
||||||
|
render(t: Builder<VerificationCompleteViewModel>, 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")
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -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 {Builder, 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: Builder<VerifyEmojisViewModel>, 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`
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -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 {Builder, 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: Builder<WaitingForOtherUserViewModel>, vm: WaitingForOtherUserViewModel) {
|
||||||
|
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")
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user