mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 19:14:52 +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;
|
||||
"members": true;
|
||||
"member": string;
|
||||
"device-verification": string | boolean;
|
||||
"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";
|
||||
|
@ -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,14 @@ export class SessionViewModel extends ViewModel {
|
||||
}));
|
||||
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");
|
||||
this.track(lightbox.subscribe(eventId => {
|
||||
this._updateLightbox(eventId);
|
||||
@ -143,7 +153,8 @@ export class SessionViewModel extends ViewModel {
|
||||
this._gridViewModel ||
|
||||
this._settingsViewModel ||
|
||||
this._createRoomViewModel ||
|
||||
this._joinRoomViewModel
|
||||
this._joinRoomViewModel ||
|
||||
this._verificationViewModel
|
||||
);
|
||||
}
|
||||
|
||||
@ -179,6 +190,10 @@ export class SessionViewModel extends ViewModel {
|
||||
return this._joinRoomViewModel;
|
||||
}
|
||||
|
||||
get verificationViewModel() {
|
||||
return this._verificationViewModel;
|
||||
}
|
||||
|
||||
get toastCollectionViewModel() {
|
||||
return this._toastCollectionViewModel;
|
||||
}
|
||||
@ -327,6 +342,17 @@ export class SessionViewModel extends ViewModel {
|
||||
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) {
|
||||
if (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 {
|
||||
const keyBackup = this._keyBackup;
|
||||
if (!keyBackup || keyBackup.version === undefined) {
|
||||
|
@ -17,9 +17,11 @@ limitations under the License.
|
||||
import {ConcatList} from "../../../observable";
|
||||
import {ViewModel, Options as BaseOptions} from "../../ViewModel";
|
||||
import {CallToastCollectionViewModel} from "./calls/CallsToastCollectionViewModel";
|
||||
import {VerificationToastCollectionViewModel} from "./verification/VerificationToastCollectionViewModel";
|
||||
import type {Session} from "../../../matrix/Session.js";
|
||||
import type {SegmentType} from "../../navigation";
|
||||
import type {BaseToastNotificationViewModel} from "./BaseToastNotificationViewModel";
|
||||
import type {IToastCollection} from "./IToastCollection";
|
||||
|
||||
type Options = {
|
||||
session: Session;
|
||||
@ -31,9 +33,16 @@ export class ToastCollectionViewModel extends ViewModel<SegmentType, Options> {
|
||||
constructor(options: Options) {
|
||||
super(options);
|
||||
const session = this.getOption("session");
|
||||
const vms = [
|
||||
this.track(new CallToastCollectionViewModel(this.childOptions({ session }))),
|
||||
].map(vm => vm.toastViewModels);
|
||||
this.toastViewModels = new ConcatList(...vms);
|
||||
const collectionVms: IToastCollection[] = [];
|
||||
if (this.features.calls) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
olmUtil: this._olmUtil,
|
||||
deviceTracker: this._deviceTracker,
|
||||
deviceMessageHandler: this._deviceMessageHandler,
|
||||
hsApi: this._hsApi,
|
||||
ownUserId: this.userId,
|
||||
e2eeAccount: this._e2eeAccount
|
||||
e2eeAccount: this._e2eeAccount,
|
||||
deviceId: this.deviceId,
|
||||
});
|
||||
if (await crossSigning.load(log)) {
|
||||
this._crossSigning.set(crossSigning);
|
||||
|
@ -19,6 +19,8 @@ import {pkSign} from "./common";
|
||||
import {SASVerification} from "./SAS/SASVerification";
|
||||
import {ToDeviceChannel} from "./SAS/channel/Channel";
|
||||
import {VerificationEventType} from "./SAS/channel/types";
|
||||
import {ObservableMap} from "../../observable/map";
|
||||
import {SASRequest} from "./SAS/SASRequest";
|
||||
import type {SecretStorage} from "../ssss/SecretStorage";
|
||||
import type {Storage} from "../storage/idb/Storage";
|
||||
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 {SignedValue, DeviceKey} from "../e2ee/common";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
|
||||
type Olm = typeof OlmNamespace;
|
||||
|
||||
// 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 readonly deviceId: string;
|
||||
private sasVerificationInProgress?: SASVerification;
|
||||
public receivedSASVerifications: ObservableMap<string, SASRequest> = new ObservableMap();
|
||||
|
||||
constructor(options: {
|
||||
storage: Storage,
|
||||
@ -115,21 +119,7 @@ export class CrossSigning {
|
||||
this.deviceMessageHandler = options.deviceMessageHandler;
|
||||
|
||||
this.deviceMessageHandler.on("message", async ({ unencrypted: unencryptedEvent }) => {
|
||||
if (this.sasVerificationInProgress &&
|
||||
(
|
||||
!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();
|
||||
});
|
||||
}
|
||||
this._handleSASDeviceMessage(unencryptedEvent);
|
||||
})
|
||||
}
|
||||
|
||||
@ -182,14 +172,18 @@ export class CrossSigning {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const otherUserId = requestOrUserId instanceof SASRequest ? requestOrUserId.sender : requestOrUserId;
|
||||
const startingMessage = requestOrUserId instanceof SASRequest ? requestOrUserId.startingMessage : undefined;
|
||||
const channel = new ToDeviceChannel({
|
||||
deviceTracker: this.deviceTracker,
|
||||
hsApi: this.hsApi,
|
||||
otherUserId: userId,
|
||||
otherUserId,
|
||||
clock: this.platform.clock,
|
||||
deviceMessageHandler: this.deviceMessageHandler,
|
||||
ourUserDeviceId: this.deviceId,
|
||||
@ -201,7 +195,7 @@ export class CrossSigning {
|
||||
olmUtil: this.olmUtil,
|
||||
ourUserId: this.ownUserId,
|
||||
ourUserDeviceId: this.deviceId,
|
||||
otherUserId: userId,
|
||||
otherUserId,
|
||||
log,
|
||||
channel,
|
||||
e2eeAccount: this.e2eeAccount,
|
||||
@ -212,6 +206,35 @@ export class CrossSigning {
|
||||
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. */
|
||||
async signOwnDevice(log: ILogItem): Promise<DeviceKey | undefined> {
|
||||
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 {DeviceTracker} from "../../e2ee/DeviceTracker.js";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
import {IChannel} from "./channel/Channel";
|
||||
import {HomeServerApi} from "../../net/HomeServerApi";
|
||||
import type {IChannel} from "./channel/Channel";
|
||||
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 {SendReadyStage} from "./stages/SendReadyStage";
|
||||
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||
import {VerificationCancelledError} from "./VerificationCancelledError";
|
||||
import {Timeout} from "../../../platform/types/types";
|
||||
import {Clock} from "../../../platform/web/dom/Clock.js";
|
||||
import {EventEmitter} from "../../../utils/EventEmitter";
|
||||
import {SASProgressEvents} from "./types";
|
||||
|
||||
@ -84,6 +84,10 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
async abort() {
|
||||
await this.channel.cancelVerification(CancelReason.UserCancelled);
|
||||
}
|
||||
|
||||
async start() {
|
||||
try {
|
||||
let stage = this.startStage;
|
||||
@ -98,6 +102,9 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (this.channel.isCancelled) {
|
||||
this.emit("VerificationCancelled", this.channel.cancellation);
|
||||
}
|
||||
this.olmSas.free();
|
||||
this.timeout.abort();
|
||||
this.finished = true;
|
||||
@ -163,6 +170,9 @@ export function tests() {
|
||||
device_id: deviceId,
|
||||
keys: {
|
||||
[`ed25519:${deviceId}`]: "D8w9mrokGdEZPdPgrU0kQkYi4vZyzKEBfvGyZsGK7+Q",
|
||||
},
|
||||
unsigned: {
|
||||
device_display_name: "lala10",
|
||||
}
|
||||
};
|
||||
},
|
||||
|
@ -49,6 +49,8 @@ export interface IChannel {
|
||||
acceptMessage: any;
|
||||
startMessage: any;
|
||||
initiatedByUs: boolean;
|
||||
isCancelled: boolean;
|
||||
cancellation?: { code: CancelReason, 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: CancelReason, cancelledByUs: boolean };
|
||||
|
||||
/**
|
||||
*
|
||||
@ -116,8 +118,12 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
||||
}
|
||||
}
|
||||
|
||||
get cancellation(): IChannel["cancellation"] {
|
||||
return this._cancellation;
|
||||
};
|
||||
|
||||
get isCancelled(): boolean {
|
||||
return this._isCancelled;
|
||||
return !!this._cancellation;
|
||||
}
|
||||
|
||||
async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void> {
|
||||
@ -199,7 +205,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
||||
return;
|
||||
}
|
||||
if (event.type === VerificationEventType.Cancel) {
|
||||
this._isCancelled = true;
|
||||
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,
|
||||
@ -243,7 +249,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
||||
}
|
||||
}
|
||||
await this.hsApi.sendToDevice(VerificationEventType.Cancel, payload, makeTxnId(), { log }).response();
|
||||
this._isCancelled = true;
|
||||
this._cancellation = { code: cancellationType, cancelledByUs: true };
|
||||
this.dispose();
|
||||
});
|
||||
}
|
||||
@ -258,7 +264,7 @@ export class ToDeviceChannel extends Disposables implements IChannel {
|
||||
}
|
||||
|
||||
waitForEvent(eventType: VerificationEventType): Promise<any> {
|
||||
if (this._isCancelled) {
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
// Check if we already received the message
|
||||
|
@ -17,6 +17,7 @@ export class MockChannel implements ITestChannel {
|
||||
public initiatedByUs: boolean;
|
||||
public startMessage: any;
|
||||
public isCancelled: boolean = false;
|
||||
public cancellation: { code: CancelReason; cancelledByUs: boolean; };
|
||||
private olmSas: any;
|
||||
|
||||
constructor(
|
||||
|
@ -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,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) {
|
||||
if (!this.allowSelection) { return; }
|
||||
const content = {
|
||||
|
@ -19,6 +19,7 @@ import {VerificationEventType} from "../channel/types";
|
||||
export class SendDoneStage extends BaseSASVerificationStage {
|
||||
async completeStage() {
|
||||
await this.log.wrap("SendDoneStage.completeStage", async (log) => {
|
||||
this.eventEmitter.emit("VerificationCompleted", this.otherUserDeviceId);
|
||||
await this.channel.send(VerificationEventType.Done, {}, log);
|
||||
});
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export class SendMacStage extends BaseSASVerificationStage {
|
||||
this.channel.id;
|
||||
|
||||
const deviceKeyId = `ed25519:${this.ourUserDeviceId}`;
|
||||
const deviceKeys = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning();
|
||||
const deviceKeys = this.e2eeAccount.getUnsignedDeviceKey();
|
||||
mac[deviceKeyId] = calculateMAC(deviceKeys.keys[deviceKeyId], baseInfo + 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
|
||||
limitations under the License.
|
||||
*/
|
||||
import {CalculateSASStage} from "./stages/CalculateSASStage";
|
||||
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||
import type {IChannel} from "./channel/Channel";
|
||||
import type {CalculateSASStage} from "./stages/CalculateSASStage";
|
||||
import type {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||
|
||||
export type SASProgressEvents = {
|
||||
SelectVerificationStage: SelectVerificationMethodStage;
|
||||
EmojiGenerated: CalculateSASStage;
|
||||
VerificationCompleted: string;
|
||||
VerificationCancelled: IChannel["cancellation"];
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="#ff00ff"/>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 658 B |
@ -1264,27 +1264,52 @@ button.RoomDetailsView_row::after {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.VerificationToastNotificationView:not(:first-child),
|
||||
.CallToastNotificationView:not(:first-child) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.VerificationToastNotificationView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.CallToastNotificationView {
|
||||
display: grid;
|
||||
grid-template-rows: 40px 1fr 1fr 48px;
|
||||
row-gap: 4px;
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
|
||||
.VerificationToastNotificationView,
|
||||
.CallToastNotificationView {
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 8px;
|
||||
color: var(--text-color);
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: auto 176px auto;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.VerificationToastNotificationView__dismiss-btn,
|
||||
.CallToastNotificationView__dismiss-btn {
|
||||
background: center var(--background-color-secondary--darker-5) url("./icons/dismiss.svg?primary=text-color") no-repeat;
|
||||
border-radius: 100%;
|
||||
@ -1292,11 +1317,16 @@ button.RoomDetailsView_row::after {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.VerificationToastNotificationView__title,
|
||||
.CallToastNotificationView__name {
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.VerificationToastNotificationView__description {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.CallToastNotificationView__description {
|
||||
margin-left: 42px;
|
||||
}
|
||||
@ -1350,7 +1380,105 @@ button.RoomDetailsView_row::after {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.VerificationToastNotificationView__action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.CallToastNotificationView__action .button-action {
|
||||
width: 100px;
|
||||
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 {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);
|
||||
|
@ -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 () => {
|
||||
await vm.signOwnDevice();
|
||||
})
|
||||
}, "Sign own device");
|
||||
return t.div([
|
||||
t.button(
|
||||
{
|
||||
onClick: disableTargetCallback(async (evt) => {
|
||||
await vm.signOwnDevice();
|
||||
}),
|
||||
},
|
||||
"Sign own device"
|
||||
),
|
||||
t.button(
|
||||
{
|
||||
onClick: disableTargetCallback(async () => {
|
||||
vm.navigateToVerification();
|
||||
}),
|
||||
},
|
||||
"Verify by emoji"
|
||||
),
|
||||
]);
|
||||
}),
|
||||
|
||||
]);
|
||||
|
@ -15,17 +15,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {CallToastNotificationView} from "./CallToastNotificationView";
|
||||
import {VerificationToastNotificationView} from "./VerificationToastNotificationView";
|
||||
import {ListView} from "../../general/ListView";
|
||||
import {TemplateView, Builder} from "../../general/TemplateView";
|
||||
import type {IView} from "../../general/types";
|
||||
import type {CallToastNotificationViewModel} from "../../../../../domain/session/toast/calls/CallToastNotificationViewModel";
|
||||
import type {ToastCollectionViewModel} from "../../../../../domain/session/toast/ToastCollectionViewModel";
|
||||
import type {BaseToastNotificationViewModel} from "../../../../../domain/session/toast/BaseToastNotificationViewModel";
|
||||
import type {VerificationToastNotificationViewModel} from "../../../../../domain/session/toast/verification/VerificationToastNotificationViewModel";
|
||||
|
||||
function toastViewModelToView(vm: BaseToastNotificationViewModel): IView {
|
||||
switch (vm.kind) {
|
||||
case "calls":
|
||||
return new CallToastNotificationView(vm as CallToastNotificationViewModel);
|
||||
case "verification":
|
||||
return new VerificationToastNotificationView(vm as VerificationToastNotificationViewModel);
|
||||
default:
|
||||
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> {
|
||||
render(t: Builder<ToastCollectionViewModel>, vm: ToastCollectionViewModel) {
|
||||
const view = new ListView({
|
||||
list: vm.toastViewModels,
|
||||
parentProvidesUpdates: false,
|
||||
}, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm));
|
||||
return t.div({ className: "ToastCollectionView" }, [
|
||||
t.view(view),
|
||||
t.ifView(vm => !!vm.toastViewModels, t => {
|
||||
return new ListView({
|
||||
list: vm.toastViewModels,
|
||||
parentProvidesUpdates: false,
|
||||
}, (vm: CallToastNotificationViewModel) => toastViewModelToView(vm));
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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