mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-11-20 03:25:52 +01:00
Merge branch 'master' into implement-secret-sharing
This commit is contained in:
commit
571e6591c8
@ -35,6 +35,7 @@ export type SegmentType = {
|
||||
"members": true;
|
||||
"member": string;
|
||||
"device-verification": string | boolean;
|
||||
"verification": string | boolean;
|
||||
"join-room": true;
|
||||
};
|
||||
|
||||
@ -60,7 +61,7 @@ function allowsChild(parent: Segment<SegmentType> | undefined, child: Segment<Se
|
||||
case "room":
|
||||
return type === "lightbox" || type === "right-panel";
|
||||
case "right-panel":
|
||||
return type === "details"|| type === "members" || type === "member";
|
||||
return type === "details"|| type === "members" || type === "member" || type === "verification";
|
||||
case "logout":
|
||||
return type === "forced";
|
||||
default:
|
||||
@ -176,7 +177,7 @@ export function parseUrlPath(urlPath: string, currentNavPath: Path<SegmentType>,
|
||||
if (sessionSegment) {
|
||||
segments.push(sessionSegment);
|
||||
}
|
||||
} else if (type === "details" || type === "members") {
|
||||
} else if (type === "details" || type === "members" || type === "verification") {
|
||||
pushRightPanelSegment(segments, type);
|
||||
} else if (type === "member") {
|
||||
let userId = iterator.next().value;
|
||||
|
@ -18,6 +18,7 @@ import {ViewModel} from "../../ViewModel";
|
||||
import {RoomType} from "../../../matrix/room/common";
|
||||
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
|
||||
import {UserTrust} from "../../../matrix/verification/CrossSigning";
|
||||
import {RoomStatus} from "../../../matrix/room/common";
|
||||
|
||||
export class MemberDetailsViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
@ -43,6 +44,8 @@ export class MemberDetailsViewModel extends ViewModel {
|
||||
get name() { return this._member.name; }
|
||||
|
||||
get userId() { return this._member.userId; }
|
||||
|
||||
get canVerifyUser() { return this._member.userId !== this._session.userId; }
|
||||
|
||||
get trustDescription() {
|
||||
switch (this._userTrust?.get()) {
|
||||
@ -107,6 +110,27 @@ export class MemberDetailsViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
async verifyUser() {
|
||||
await this.logger.run("MemberDetailsViewModel.verifyUser", async () => {
|
||||
const room = this._session.findDirectMessageForUserId(this.userId);
|
||||
let roomId = room?.id;
|
||||
if (!roomId) {
|
||||
const roomBeingCreated = await this._session.createRoom({
|
||||
type: RoomType.DirectMessage,
|
||||
invites: [this.userId]
|
||||
});
|
||||
roomId = roomBeingCreated.roomId;
|
||||
}
|
||||
const observable = await this._session.observeRoomStatus(roomId);
|
||||
await observable.waitFor(s => s === RoomStatus.Joined).promise;
|
||||
let path = this.navigation.path.until("session");
|
||||
path = path.with(this.navigation.segment("room", roomId));
|
||||
path = path.with(this.navigation.segment("right-panel", true));
|
||||
path = path.with(this.navigation.segment("verification", this.userId));
|
||||
this.navigation.applyPath(path);
|
||||
});
|
||||
}
|
||||
|
||||
get avatarLetter() {
|
||||
return avatarInitials(this.name);
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import {ViewModel} from "../../ViewModel";
|
||||
import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js";
|
||||
import {MemberListViewModel} from "./MemberListViewModel.js";
|
||||
import {MemberDetailsViewModel} from "./MemberDetailsViewModel.js";
|
||||
import {DeviceVerificationViewModel} from "../verification/DeviceVerificationViewModel";
|
||||
|
||||
export class RightPanelViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
@ -68,16 +69,37 @@ export class RightPanelViewModel extends ViewModel {
|
||||
this.urlRouter.pushUrl(url);
|
||||
}
|
||||
);
|
||||
this._hookUpdaterToSegment("verification", DeviceVerificationViewModel, () => {
|
||||
const options = {
|
||||
session: this._session,
|
||||
room: this._room,
|
||||
};
|
||||
const id = this.navigation.path.get("verification").value;
|
||||
if (typeof id === "string") {
|
||||
/**
|
||||
* Here id is:
|
||||
* 1. id of the request when we receive a sas verification request
|
||||
* 2. id of the user we are trying to verify when we initiate the verification process
|
||||
*/
|
||||
const request = this._session?.crossSigning.get()?.receivedSASVerifications.get(id);
|
||||
const extraOptions = request ? { request } : { userId: id };
|
||||
Object.assign(options, extraOptions);
|
||||
}
|
||||
return options;
|
||||
});
|
||||
}
|
||||
|
||||
_hookUpdaterToSegment(segment, viewmodel, argCreator, failCallback) {
|
||||
async _hookUpdaterToSegment(segment, ViewModel, argCreator, failCallback) {
|
||||
const observable = this.navigation.observe(segment);
|
||||
const updater = this._setupUpdater(segment, viewmodel, argCreator, failCallback);
|
||||
const updater = await this._setupUpdater(segment, ViewModel, argCreator, failCallback);
|
||||
this.track(observable.subscribe(updater));
|
||||
}
|
||||
|
||||
_setupUpdater(segment, viewmodel, argCreator, failCallback) {
|
||||
async _setupUpdater(segment, ViewModel, argCreator, failCallback) {
|
||||
const updater = async (skipDispose = false) => {
|
||||
if (this._activeViewModel instanceof ViewModel) {
|
||||
return;
|
||||
}
|
||||
if (!skipDispose) {
|
||||
this._activeViewModel = this.disposeTracked(this._activeViewModel);
|
||||
}
|
||||
@ -88,11 +110,11 @@ export class RightPanelViewModel extends ViewModel {
|
||||
failCallback();
|
||||
return;
|
||||
}
|
||||
this._activeViewModel = this.track(new viewmodel(this.childOptions(args)));
|
||||
this._activeViewModel = this.track(new ViewModel(this.childOptions(args)));
|
||||
}
|
||||
this.emitChange("activeViewModel");
|
||||
};
|
||||
updater(true);
|
||||
await updater(true);
|
||||
return updater;
|
||||
}
|
||||
|
||||
|
@ -77,9 +77,10 @@ export class RoomViewModel extends ErrorReportViewModel {
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.logAndCatch("RoomViewModel.load", async log => {
|
||||
await this.logAndCatch("RoomViewModel.load", async log => {
|
||||
this._room.on("change", this._onRoomChange);
|
||||
const timeline = await this._room.openTimeline(log);
|
||||
this.track(() => timeline.release());
|
||||
this._tileOptions = this.childOptions({
|
||||
session: this.getOption("session"),
|
||||
roomVM: this,
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SimpleTile} from "./SimpleTile.js";
|
||||
import {SimpleTile} from "./SimpleTile";
|
||||
import {ReactionsViewModel} from "../ReactionsViewModel.js";
|
||||
import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar";
|
||||
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SimpleTile} from "./SimpleTile.js";
|
||||
import {SimpleTile} from "./SimpleTile";
|
||||
import {ViewModel} from "../../../../ViewModel";
|
||||
import {LocalMedia} from "../../../../../matrix/calls/LocalMedia";
|
||||
import {CallType} from "../../../../../matrix/calls/callEventTypes";
|
||||
|
@ -3,6 +3,7 @@ import {UpdateAction} from "../UpdateAction";
|
||||
import {BaseEntry} from "../../../../../matrix/room/timeline/entries/BaseEntry";
|
||||
import {BaseEventEntry} from "../../../../../matrix/room/timeline/entries/BaseEventEntry";
|
||||
import {ViewModel} from "../../../../ViewModel";
|
||||
import type {SegmentType} from "../../../../navigation";
|
||||
import type {Options} from "../../../../ViewModel";
|
||||
|
||||
/**
|
||||
@ -14,12 +15,12 @@ import type {Options} from "../../../../ViewModel";
|
||||
* a pending event is removed on remote echo.
|
||||
* */
|
||||
|
||||
export class DateTile extends ViewModel implements ITile<BaseEventEntry> {
|
||||
export class DateTile<T extends object = SegmentType> extends ViewModel<T, Options<T>> implements ITile<BaseEventEntry> {
|
||||
private _emitUpdate?: EmitUpdateFn;
|
||||
private _dateString?: string;
|
||||
private _machineReadableString?: string;
|
||||
|
||||
constructor(private _firstTileInDay: ITile<BaseEventEntry>, options: Options) {
|
||||
constructor(private _firstTileInDay: ITile<BaseEventEntry>, options: Options<T>) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
@ -168,11 +169,13 @@ import { SimpleTile } from "./SimpleTile";
|
||||
export function tests() {
|
||||
return {
|
||||
"date tile sorts before reference tile": assert => {
|
||||
//@ts-ignore
|
||||
const a = new SimpleTile(new EventEntry({
|
||||
event: {},
|
||||
eventIndex: 2,
|
||||
fragmentId: 1
|
||||
}, undefined), {});
|
||||
//@ts-ignore
|
||||
const b = new SimpleTile(new EventEntry({
|
||||
event: {},
|
||||
eventIndex: 3,
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SimpleTile} from "./SimpleTile.js";
|
||||
import {SimpleTile} from "./SimpleTile";
|
||||
|
||||
export class EncryptionEnabledTile extends SimpleTile {
|
||||
get shape() {
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SimpleTile} from "./SimpleTile.js";
|
||||
import {SimpleTile} from "./SimpleTile";
|
||||
import {UpdateAction} from "../UpdateAction.js";
|
||||
import {ConnectionError} from "../../../../../matrix/error.js";
|
||||
import {ConnectionStatus} from "../../../../../matrix/net/Reconnector";
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {UpdateAction} from "../UpdateAction.js";
|
||||
import {BaseEntry} from "../../../../../matrix/room/timeline/entries/BaseEntry";
|
||||
import {BaseEventEntry} from "../../../../../matrix/room/timeline/entries/BaseEventEntry";
|
||||
import {IDisposable} from "../../../../../utils/Disposables";
|
||||
|
||||
export type EmitUpdateFn = (tile: ITile<BaseEntry>, props: any) => void
|
||||
@ -18,6 +17,7 @@ export enum TileShape {
|
||||
Video = "video",
|
||||
DateHeader = "date-header",
|
||||
Call = "call",
|
||||
Verification = "verification",
|
||||
}
|
||||
|
||||
// TODO: should we imply inheriting from view model here?
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SimpleTile} from "./SimpleTile.js";
|
||||
import {SimpleTile} from "./SimpleTile";
|
||||
|
||||
export class RoomMemberTile extends SimpleTile {
|
||||
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SimpleTile} from "./SimpleTile.js";
|
||||
import {SimpleTile} from "./SimpleTile";
|
||||
|
||||
export class RoomNameTile extends SimpleTile {
|
||||
|
||||
|
@ -16,24 +16,38 @@ limitations under the License.
|
||||
|
||||
import {UpdateAction} from "../UpdateAction.js";
|
||||
import {ErrorReportViewModel} from "../../../../ErrorReportViewModel";
|
||||
import {TileShape} from "./ITile";
|
||||
import {ITile, TileShape} from "./ITile";
|
||||
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
||||
import {DateTile} from "./DateTile";
|
||||
import {Options as BaseOptions} from "../../../../ErrorReportViewModel";
|
||||
import {EventEntry} from "../../../../../matrix/room/timeline/entries/EventEntry.js";
|
||||
import type {RoomViewModel} from "../../RoomViewModel.js";
|
||||
import type {Timeline} from "../../../../../matrix/room/timeline/Timeline.js";
|
||||
import type {SegmentType} from "../../../../navigation/index.js";
|
||||
|
||||
export class SimpleTile extends ErrorReportViewModel {
|
||||
constructor(entry, options) {
|
||||
export type Options<T extends object = SegmentType> = {
|
||||
roomVM: RoomViewModel;
|
||||
timeline: Timeline;
|
||||
} & BaseOptions<T>;
|
||||
|
||||
type EmitUpdate<T extends object = SegmentType> = (tile: SimpleTile<T>, params: any) => void;
|
||||
|
||||
|
||||
export abstract class SimpleTile<T extends object = SegmentType> extends ErrorReportViewModel<T, Options<T>> implements ITile {
|
||||
private _entry: EventEntry;
|
||||
private _date?: Date;
|
||||
private _needsDateSeparator: boolean = false;
|
||||
private _emitUpdate?: EmitUpdate<T>;
|
||||
|
||||
constructor(entry: EventEntry, options: Options<T>) {
|
||||
super(options);
|
||||
this._entry = entry;
|
||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : undefined;
|
||||
this._needsDateSeparator = false;
|
||||
this._emitUpdate = undefined;
|
||||
}
|
||||
// view model props for all subclasses
|
||||
// hmmm, could also do instanceof ... ?
|
||||
get shape() {
|
||||
return null;
|
||||
// "gap" | "message" | "image" | ... ?
|
||||
}
|
||||
// "gap" | "message" | "image" | ... ?
|
||||
abstract get shape(): TileShape;
|
||||
|
||||
// don't show display name / avatar
|
||||
// probably only for BaseMessageTiles of some sort?
|
||||
@ -49,7 +63,7 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
return new DateTile(this, this.childOptions({}));
|
||||
}
|
||||
|
||||
_updateDateSeparator(prev) {
|
||||
_updateDateSeparator(prev: SimpleTile) {
|
||||
if (prev && prev._date && this._date) {
|
||||
this._needsDateSeparator = prev._date.getFullYear() !== this._date.getFullYear() ||
|
||||
prev._date.getMonth() !== this._date.getMonth() ||
|
||||
@ -80,12 +94,12 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
!this._entry.pendingEvent.hasStartedSending;
|
||||
}
|
||||
|
||||
abortSending() {
|
||||
this._entry.pendingEvent?.abort();
|
||||
async abortSending(): Promise<void> {
|
||||
await this._entry.pendingEvent?.abort();
|
||||
}
|
||||
|
||||
// TilesCollection contract below
|
||||
setUpdateEmit(emitUpdate) {
|
||||
setUpdateEmit(emitUpdate?: EmitUpdate<T>) {
|
||||
this._emitUpdate = emitUpdate;
|
||||
}
|
||||
|
||||
@ -114,7 +128,7 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
compare(tile) {
|
||||
compare(tile: SimpleTile<T>) {
|
||||
if (tile.comparisonIsNotCommutative) {
|
||||
return -tile.compare(this);
|
||||
} else {
|
||||
@ -122,7 +136,7 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
compareEntry(entry) {
|
||||
compareEntry(entry: EventEntry) {
|
||||
return this._entry.compare(entry);
|
||||
}
|
||||
|
||||
@ -163,7 +177,7 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
notifyVisible() {}
|
||||
|
||||
dispose() {
|
||||
this.setUpdateEmit(null);
|
||||
this.setUpdateEmit(undefined);
|
||||
super.dispose();
|
||||
}
|
||||
// TilesCollection contract above
|
||||
@ -173,11 +187,11 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
}
|
||||
|
||||
get _roomVM() {
|
||||
return this._options.roomVM;
|
||||
return this.options.roomVM;
|
||||
}
|
||||
|
||||
get _timeline() {
|
||||
return this._options.timeline;
|
||||
return this.options.timeline;
|
||||
}
|
||||
|
||||
get _powerLevels() {
|
||||
@ -185,7 +199,7 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
}
|
||||
|
||||
get _ownMember() {
|
||||
return this._options.timeline.me;
|
||||
return this.options.timeline.me;
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
@ -197,7 +211,6 @@ export class SimpleTile extends ErrorReportViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
import { EventEntry } from "../../../../../matrix/room/timeline/entries/EventEntry.js";
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
@ -216,7 +229,9 @@ export function tests() {
|
||||
content: {}
|
||||
}
|
||||
}, undefined);
|
||||
//@ts-ignore
|
||||
const fridayTile = new SimpleTile(fridayEntry, {});
|
||||
//@ts-ignore
|
||||
const thursdayTile = new SimpleTile(thursdayEntry, {});
|
||||
assert.equal(fridayTile.needsDateSeparator, false);
|
||||
fridayTile.updatePreviousSibling(thursdayTile);
|
||||
@ -237,7 +252,9 @@ export function tests() {
|
||||
content: {}
|
||||
}
|
||||
}, undefined);
|
||||
//@ts-ignore
|
||||
const fridayTile = new SimpleTile(fridayEntry, {});
|
||||
//@ts-ignore
|
||||
const thursdayTile = new SimpleTile(thursdayEntry, {});
|
||||
assert.equal(fridayTile.needsDateSeparator, false);
|
||||
fridayTile.updatePreviousSibling(thursdayTile);
|
||||
@ -251,6 +268,7 @@ export function tests() {
|
||||
content: {}
|
||||
}
|
||||
}, undefined);
|
||||
//@ts-ignore
|
||||
const fridayTile = new SimpleTile(fridayEntry, {});
|
||||
assert.equal(fridayTile.needsDateSeparator, false);
|
||||
fridayTile.updatePreviousSibling(undefined);
|
107
src/domain/session/room/timeline/tiles/VerificationTile.ts
Normal file
107
src/domain/session/room/timeline/tiles/VerificationTile.ts
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
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 {SASRequest} from "../../../../../matrix/verification/SAS/SASRequest";
|
||||
import {TileShape} from "./ITile";
|
||||
import {SimpleTile} from "./SimpleTile";
|
||||
import {UpdateAction} from "../UpdateAction.js"
|
||||
import {VerificationEventType} from "../../../../../matrix/verification/SAS/channel/types";
|
||||
import type {EventEntry} from "../../../../../matrix/room/timeline/entries/EventEntry.js";
|
||||
import type {Options} from "./SimpleTile";
|
||||
|
||||
export const enum Status {
|
||||
Ready,
|
||||
InProgress,
|
||||
Completed,
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
export class VerificationTile extends SimpleTile {
|
||||
private request: SASRequest;
|
||||
public isCancelledByUs: boolean;
|
||||
public status: Status = Status.Ready;
|
||||
|
||||
constructor(entry: EventEntry, options: Options) {
|
||||
super(entry, options);
|
||||
this.request = new SASRequest(this.lowerEntry);
|
||||
// Calculate status based on available context-for entries
|
||||
// Needed so that tiles reflect their final status when
|
||||
// events are loaded from storage i.e after a reload.
|
||||
this.updateStatusFromAvailableContextForEntries();
|
||||
}
|
||||
|
||||
get shape(): TileShape {
|
||||
return TileShape.Verification;
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.i18n`${this.sender} wants to verify`;
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
const crossSigning = this.getOption("session").crossSigning.get();
|
||||
crossSigning.receivedSASVerifications.set(this.eventId, this.request);
|
||||
this.openVerificationPanel(this.eventId);
|
||||
}
|
||||
|
||||
async reject(): Promise<void> {
|
||||
await this.logAndCatch("VerificationTile.reject", async (log) => {
|
||||
const crossSigning = this.getOption("session").crossSigning.get();
|
||||
await this.request.reject(crossSigning, this._room, log);
|
||||
});
|
||||
}
|
||||
|
||||
private openVerificationPanel(eventId: string): void {
|
||||
let path = this.navigation.path.until("room");
|
||||
path = path.with(this.navigation.segment("right-panel", true))!;
|
||||
path = path.with(this.navigation.segment("verification", eventId))!;
|
||||
this.navigation.applyPath(path);
|
||||
}
|
||||
|
||||
updateEntry(entry: EventEntry, param: any) {
|
||||
if (param === "context-added") {
|
||||
/**
|
||||
* We received a new contextForEntry, maybe it tells us that
|
||||
* this request was cancelled or that the verification is completed?
|
||||
* Let's check.
|
||||
*/
|
||||
if (this.updateStatusFromAvailableContextForEntries()) {
|
||||
return UpdateAction.Update(param);
|
||||
}
|
||||
return UpdateAction.Nothing();
|
||||
}
|
||||
return super.updateEntry(entry, param);
|
||||
}
|
||||
|
||||
private updateStatusFromAvailableContextForEntries(): boolean {
|
||||
let needsUpdate = false;
|
||||
for (const e of this.lowerEntry.contextForEntries ?? []) {
|
||||
switch (e.eventType) {
|
||||
case VerificationEventType.Cancel:
|
||||
this.status = Status.Cancelled;
|
||||
this.isCancelledByUs = e.sender === this.getOption("session").userId;
|
||||
return true;
|
||||
case VerificationEventType.Done:
|
||||
this.status = Status.Completed;
|
||||
return true;
|
||||
default:
|
||||
this.status = Status.InProgress;
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
return needsUpdate;
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ import {EncryptedEventTile} from "./EncryptedEventTile.js";
|
||||
import {EncryptionEnabledTile} from "./EncryptionEnabledTile.js";
|
||||
import {MissingAttachmentTile} from "./MissingAttachmentTile.js";
|
||||
import {CallTile} from "./CallTile.js";
|
||||
import {VerificationTile} from "./VerificationTile.js";
|
||||
|
||||
import type {ITile, TileShape} from "./ITile";
|
||||
import type {Room} from "../../../../../matrix/room/Room";
|
||||
@ -73,6 +74,13 @@ export function tileClassForEntry(entry: TimelineEntry, options: Options): TileC
|
||||
return FileTile;
|
||||
case "m.location":
|
||||
return LocationTile;
|
||||
case "m.key.verification.request":
|
||||
const isCrossSigningDisabled = !options.session.features.crossSigning;
|
||||
const userId = options.session.userId;
|
||||
if (isCrossSigningDisabled || entry.sender === userId) {
|
||||
return undefined;
|
||||
}
|
||||
return VerificationTile as unknown as TileConstructor;
|
||||
default:
|
||||
// unknown msgtype not rendered
|
||||
return undefined;
|
||||
|
@ -43,6 +43,10 @@ export class VerificationToastCollectionViewModel extends ViewModel<SegmentType,
|
||||
|
||||
|
||||
async onAdd(_, request: SASRequest) {
|
||||
if (request.sender !== this.getOption("session").userId) {
|
||||
// Don't show toast for cross-signing other users
|
||||
return;
|
||||
}
|
||||
const dismiss = () => {
|
||||
const idx = this.toastViewModels.array.findIndex(vm => vm.request.id === request.id);
|
||||
if (idx !== -1) {
|
||||
|
@ -28,10 +28,13 @@ import type {SASVerification} from "../../../matrix/verification/SAS/SASVerifica
|
||||
import type {SASRequest} from "../../../matrix/verification/SAS/SASRequest";
|
||||
import type {CrossSigning} from "../../../matrix/verification/CrossSigning";
|
||||
import type {ILogItem} from "../../../logging/types";
|
||||
import type {Room} from "../../../matrix/room/Room.js";
|
||||
|
||||
type Options = BaseOptions & {
|
||||
session: Session;
|
||||
request: SASRequest;
|
||||
request?: SASRequest;
|
||||
room?: Room;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
const neededSecrets = [
|
||||
@ -47,20 +50,26 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel<SegmentTyp
|
||||
|
||||
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);
|
||||
}
|
||||
this.start(options);
|
||||
}
|
||||
|
||||
private async start(requestOrUserId: SASRequest | string) {
|
||||
private async start(options: Options): Promise<void> {
|
||||
const room = options.room;
|
||||
let requestOrUserId: SASRequest | string;
|
||||
requestOrUserId =
|
||||
options.request ??
|
||||
options.userId ??
|
||||
this.getOption("session").userId;
|
||||
await this.startVerification(requestOrUserId, room);
|
||||
}
|
||||
|
||||
private async startVerification(requestOrUserId: SASRequest | string, room?: Room) {
|
||||
await this.logAndCatch("DeviceVerificationViewModel.start", async (log) => {
|
||||
const crossSigning = this.getOption("session").crossSigning.get() as CrossSigning;
|
||||
this.sas = crossSigning.startVerification(requestOrUserId, log)!;
|
||||
const crossSigning = this.getOption("session").crossSigning.get();
|
||||
this.sas = crossSigning.startVerification(requestOrUserId, room, log);
|
||||
if (!this.sas) {
|
||||
throw new Error("CrossSigning.startVerification did not return a sas object!");
|
||||
}
|
||||
if (!await this.performPreVerificationChecks(crossSigning, log)) {
|
||||
return;
|
||||
}
|
||||
@ -68,7 +77,12 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel<SegmentTyp
|
||||
if (typeof requestOrUserId === "string") {
|
||||
this.updateCurrentStageViewModel(new WaitingForOtherUserViewModel(this.childOptions({ sas: this.sas })));
|
||||
}
|
||||
return this.sas.start();
|
||||
if (this.sas.isCrossSigningAnotherUser) {
|
||||
return crossSigning.signUser(this.sas, log);
|
||||
}
|
||||
else {
|
||||
return crossSigning.signDevice(this.sas, log);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -117,13 +131,13 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel<SegmentTyp
|
||||
this.track(this.sas.disposableOn("VerificationCancelled", (cancellation) => {
|
||||
this.updateCurrentStageViewModel(
|
||||
new VerificationCancelledViewModel(
|
||||
this.childOptions({ cancellation: cancellation! })
|
||||
this.childOptions({ cancellation: cancellation!, sas: this.sas })
|
||||
)
|
||||
);
|
||||
}));
|
||||
this.track(this.sas.disposableOn("VerificationCompleted", (deviceId) => {
|
||||
this.updateCurrentStageViewModel(
|
||||
new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId! }))
|
||||
new VerificationCompleteViewModel(this.childOptions({ deviceId: deviceId!, sas: this.sas }))
|
||||
);
|
||||
this.requestSecrets();
|
||||
}));
|
||||
@ -155,4 +169,8 @@ export class DeviceVerificationViewModel extends ErrorReportViewModel<SegmentTyp
|
||||
get currentStageViewModel() {
|
||||
return this._currentStageViewModel;
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return "verification";
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
type Options = BaseOptions & {
|
||||
sas: SASVerification;
|
||||
session: Session;
|
||||
};
|
||||
|
||||
export abstract class DismissibleVerificationViewModel<O extends Options> extends ErrorReportViewModel<SegmentType, O> {
|
||||
dismiss(): void {
|
||||
/**
|
||||
* If we're cross-signing another user, redirect to the room (which will just close the right panel).
|
||||
* If we're verifying a device, redirect to settings.
|
||||
*/
|
||||
if (this.getOption("sas").isCrossSigningAnotherUser) {
|
||||
const path = this.navigation.path.until("room");
|
||||
this.navigation.applyPath(path);
|
||||
} else {
|
||||
this.navigation.push("settings", true);
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,15 @@ export class SelectMethodViewModel extends ErrorReportViewModel<SegmentType, Opt
|
||||
return this.options.stage.otherDeviceName;
|
||||
}
|
||||
|
||||
get otherUserId() {
|
||||
return this.getOption("sas").otherUserId;
|
||||
}
|
||||
|
||||
get kind(): string {
|
||||
return "select-method";
|
||||
}
|
||||
|
||||
get isCrossSigningAnotherUser(): boolean {
|
||||
return this.getOption("sas").isCrossSigningAnotherUser;
|
||||
}
|
||||
}
|
||||
|
@ -14,16 +14,20 @@ 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 {Options as BaseOptions} from "../../../ViewModel";
|
||||
import {DismissibleVerificationViewModel} from "./DismissibleVerificationViewModel";
|
||||
import type {CancelReason} from "../../../../matrix/verification/SAS/channel/types";
|
||||
import type {IChannel} from "../../../../matrix/verification/SAS/channel/Channel";
|
||||
import type {Session} from "../../../../matrix/Session.js";
|
||||
import type {IChannel} from "../../../../matrix/verification/SAS/channel/IChannel";
|
||||
import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification";
|
||||
|
||||
type Options = BaseOptions & {
|
||||
cancellation: IChannel["cancellation"];
|
||||
session: Session;
|
||||
sas: SASVerification;
|
||||
};
|
||||
|
||||
export class VerificationCancelledViewModel extends ViewModel<SegmentType, Options> {
|
||||
export class VerificationCancelledViewModel extends DismissibleVerificationViewModel<Options> {
|
||||
get cancelCode(): CancelReason {
|
||||
return this.options.cancellation!.code;
|
||||
}
|
||||
@ -32,10 +36,6 @@ export class VerificationCancelledViewModel extends ViewModel<SegmentType, Optio
|
||||
return this.options.cancellation!.cancelledByUs;
|
||||
}
|
||||
|
||||
gotoSettings() {
|
||||
this.navigation.push("settings", true);
|
||||
}
|
||||
|
||||
get kind(): string {
|
||||
return "verification-cancelled";
|
||||
}
|
||||
|
@ -14,26 +14,36 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SegmentType} from "../../../navigation/index";
|
||||
import {ErrorReportViewModel} from "../../../ErrorReportViewModel";
|
||||
import {DismissibleVerificationViewModel} from "./DismissibleVerificationViewModel";
|
||||
import type {Options as BaseOptions} from "../../../ViewModel";
|
||||
import type {Session} from "../../../../matrix/Session.js";
|
||||
import type {SASVerification} from "../../../../matrix/verification/SAS/SASVerification";
|
||||
|
||||
type Options = BaseOptions & {
|
||||
deviceId: string;
|
||||
session: Session;
|
||||
sas: SASVerification;
|
||||
};
|
||||
|
||||
export class VerificationCompleteViewModel extends ErrorReportViewModel<SegmentType, Options> {
|
||||
export class VerificationCompleteViewModel extends DismissibleVerificationViewModel<Options> {
|
||||
get otherDeviceId(): string {
|
||||
return this.options.deviceId;
|
||||
}
|
||||
|
||||
gotoSettings() {
|
||||
this.navigation.push("settings", true);
|
||||
get otherUsername(): string {
|
||||
return this.getOption("sas").otherUserId;
|
||||
}
|
||||
|
||||
get kind(): string {
|
||||
return "verification-completed";
|
||||
}
|
||||
|
||||
get verificationSuccessfulMessage(): string {
|
||||
if (this.getOption("sas").isCrossSigningAnotherUser) {
|
||||
return this.i18n`You successfully verified user ${this.otherUsername}`;
|
||||
}
|
||||
else {
|
||||
return this.i18n`You successfully verified device ${this.otherDeviceId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export {RoomMemberTile} from "./domain/session/room/timeline/tiles/RoomMemberTil
|
||||
export {EncryptedEventTile} from "./domain/session/room/timeline/tiles/EncryptedEventTile.js";
|
||||
export {EncryptionEnabledTile} from "./domain/session/room/timeline/tiles/EncryptionEnabledTile.js";
|
||||
export {MissingAttachmentTile} from "./domain/session/room/timeline/tiles/MissingAttachmentTile.js";
|
||||
export {SimpleTile} from "./domain/session/room/timeline/tiles/SimpleTile.js";
|
||||
export {SimpleTile} from "./domain/session/room/timeline/tiles/SimpleTile";
|
||||
|
||||
export {TimelineView} from "./platform/web/ui/session/room/TimelineView";
|
||||
export {viewClassForTile} from "./platform/web/ui/session/room/common";
|
||||
|
@ -135,6 +135,10 @@ export class Session {
|
||||
return this._callHandler;
|
||||
}
|
||||
|
||||
get features() {
|
||||
return this._features;
|
||||
}
|
||||
|
||||
_setupCallHandler() {
|
||||
this._callHandler = new CallHandler({
|
||||
clock: this._platform.clock,
|
||||
@ -725,9 +729,9 @@ export class Session {
|
||||
return this._roomsBeingCreated;
|
||||
}
|
||||
|
||||
createRoom(options) {
|
||||
async createRoom(options) {
|
||||
let roomBeingCreated;
|
||||
this._platform.logger.runDetached("create room", async log => {
|
||||
await this._platform.logger.run("create room", async log => {
|
||||
const id = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`;
|
||||
roomBeingCreated = new RoomBeingCreated(
|
||||
id, options, this._roomsBeingCreatedUpdateCallback,
|
||||
|
@ -59,6 +59,7 @@ export class BaseRoom extends EventEmitter {
|
||||
this._powerLevels = null;
|
||||
this._powerLevelLoading = null;
|
||||
this._observedMembers = null;
|
||||
this._timelineLoadPromise = null;
|
||||
}
|
||||
|
||||
async observeStateType(type, txn = undefined) {
|
||||
@ -541,11 +542,25 @@ export class BaseRoom extends EventEmitter {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
openTimeline(log = null) {
|
||||
return this._platform.logger.wrapOrRun(log, "open timeline", async log => {
|
||||
async openTimeline(log = null) {
|
||||
return await this._platform.logger.wrapOrRun(log, "open timeline", async log => {
|
||||
if (this._timelineLoadPromise) {
|
||||
// This is to prevent races between multiple calls to this method
|
||||
await this._timelineLoadPromise;
|
||||
}
|
||||
let resolve;
|
||||
this._timelineLoadPromise = new Promise(r => {
|
||||
resolve = () => {
|
||||
this._timelineLoadPromise = null;
|
||||
r();
|
||||
};
|
||||
});
|
||||
log.set("id", this.id);
|
||||
if (this._timeline) {
|
||||
throw new Error("not dealing with load race here for now");
|
||||
log.log({ l: "Returning existing timeline" });
|
||||
resolve();
|
||||
this._timeline.retain();
|
||||
return this._timeline;
|
||||
}
|
||||
this._timeline = new Timeline({
|
||||
roomId: this.id,
|
||||
@ -572,7 +587,10 @@ export class BaseRoom extends EventEmitter {
|
||||
// this also clears this._timeline in the closeCallback
|
||||
this._timeline.dispose();
|
||||
throw err;
|
||||
} finally {
|
||||
resolve();
|
||||
}
|
||||
this._timeline.retain();
|
||||
return this._timeline;
|
||||
});
|
||||
}
|
||||
@ -610,7 +628,6 @@ export class BaseRoom extends EventEmitter {
|
||||
|
||||
dispose() {
|
||||
this._roomEncryption?.dispose();
|
||||
this._timeline?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
import {createEnum} from "../../../utils/enum";
|
||||
import {AbortError} from "../../../utils/error";
|
||||
import {Deferred} from "../../../utils/Deferred";
|
||||
import {REDACTION_TYPE} from "../common";
|
||||
import {getRelationFromContent, getRelationTarget, setRelationTarget} from "../timeline/relations.js";
|
||||
|
||||
@ -40,6 +41,7 @@ export class PendingEvent {
|
||||
this._status = SendStatus.Waiting;
|
||||
this._sendRequest = null;
|
||||
this._attachmentsTotalBytes = 0;
|
||||
this._deferred = new Deferred()
|
||||
if (this._attachments) {
|
||||
this._attachmentsTotalBytes = Object.values(this._attachments).reduce((t, a) => t + a.size, 0);
|
||||
}
|
||||
@ -228,11 +230,16 @@ export class PendingEvent {
|
||||
this._sendRequest = null;
|
||||
// both /send and /redact have the same response format
|
||||
this._data.remoteId = response.event_id;
|
||||
this._deferred.resolve(response.event_id);
|
||||
log.set("id", this._data.remoteId);
|
||||
this._status = SendStatus.Sent;
|
||||
this._emitUpdate("status");
|
||||
}
|
||||
|
||||
getRemoteId() {
|
||||
return this._deferred.promise;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._attachments) {
|
||||
for (const attachment of Object.values(this._attachments)) {
|
||||
|
@ -225,7 +225,7 @@ export class SendQueue {
|
||||
}
|
||||
}
|
||||
}
|
||||
await this._enqueueEvent(eventType, content, attachments, relatedTxnId, null, log);
|
||||
return await this._enqueueEvent(eventType, content, attachments, relatedTxnId, null, log);
|
||||
}
|
||||
|
||||
async _enqueueEvent(eventType, content, attachments, relatedTxnId, relatedEventId, log) {
|
||||
@ -239,6 +239,7 @@ export class SendQueue {
|
||||
if (this._sendLoopLogItem) {
|
||||
log.refDetached(this._sendLoopLogItem);
|
||||
}
|
||||
return pendingEvent;
|
||||
}
|
||||
|
||||
async enqueueRedaction(eventIdOrTxnId, reason, log) {
|
||||
|
@ -25,9 +25,11 @@ import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js";
|
||||
import {REDACTION_TYPE} from "../common";
|
||||
import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js";
|
||||
import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js";
|
||||
import {RetainedValue} from "../../../utils/RetainedValue";
|
||||
|
||||
export class Timeline {
|
||||
export class Timeline extends RetainedValue {
|
||||
constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi}) {
|
||||
super(() => { this.dispose(); });
|
||||
this._roomId = roomId;
|
||||
this._storage = storage;
|
||||
this._closeCallback = closeCallback;
|
||||
@ -320,15 +322,18 @@ export class Timeline {
|
||||
}
|
||||
const id = entry.contextEventId;
|
||||
// before looking into remoteEntries, check the entries
|
||||
// that about to be added first
|
||||
// that are about to be added first
|
||||
let contextEvent = entries.find(e => e.id === id);
|
||||
if (!contextEvent) {
|
||||
contextEvent = this._findLoadedEventById(id);
|
||||
}
|
||||
if (contextEvent) {
|
||||
entry.setContextEntry(contextEvent);
|
||||
// we don't emit an update here, as the add or update
|
||||
// we don't emit an update for `entry` here, as the add or update
|
||||
// that the callee will emit hasn't been emitted yet.
|
||||
// however we do emit an update for the `contextEvent` so that it
|
||||
// can do something in response to `entry` being added (if needed).
|
||||
this._emitUpdateForEntry(contextEvent, "context-added");
|
||||
} else {
|
||||
// we don't await here, which is not ideal,
|
||||
// but one of our callers, addEntries, is not async
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import {BaseEntry} from "./BaseEntry";
|
||||
import {REDACTION_TYPE} from "../../common";
|
||||
import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent} from "../relations.js";
|
||||
import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent, REFERENCE_RELATION_TYPE} from "../relations.js";
|
||||
import {PendingAnnotation} from "../PendingAnnotation.js";
|
||||
import {createReplyContent} from "./reply.js"
|
||||
|
||||
@ -35,6 +35,10 @@ export class BaseEventEntry extends BaseEntry {
|
||||
return !!this.relation?.["m.in_reply_to"];
|
||||
}
|
||||
|
||||
get isReference() {
|
||||
return this.relation?.rel_type === REFERENCE_RELATION_TYPE;
|
||||
}
|
||||
|
||||
get isRedacting() {
|
||||
return !!this._pendingRedactions;
|
||||
}
|
||||
|
@ -148,9 +148,9 @@ export class EventEntry extends BaseEventEntry {
|
||||
return originalRelation || getRelationFromContent(this.content);
|
||||
}
|
||||
|
||||
// similar to relatedEventID but only for replies
|
||||
// similar to relatedEventID but only for replies and reference relations
|
||||
get contextEventId() {
|
||||
if (this.isReply) {
|
||||
if (this.isReply || this.isReference) {
|
||||
return this.relatedEventId;
|
||||
}
|
||||
return null;
|
||||
|
@ -18,6 +18,7 @@ import {REDACTION_TYPE} from "../common";
|
||||
|
||||
export const REACTION_TYPE = "m.reaction";
|
||||
export const ANNOTATION_RELATION_TYPE = "m.annotation";
|
||||
export const REFERENCE_RELATION_TYPE = "m.reference";
|
||||
|
||||
export function createAnnotation(targetId, key) {
|
||||
return {
|
||||
@ -29,6 +30,15 @@ export function createAnnotation(targetId, key) {
|
||||
};
|
||||
}
|
||||
|
||||
export function createReference(targetId) {
|
||||
return {
|
||||
"m.relates_to": {
|
||||
"event_id": targetId,
|
||||
"rel_type": REFERENCE_RELATION_TYPE
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getRelationTarget(relation) {
|
||||
return relation.event_id || relation["m.in_reply_to"]?.event_id
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ import {verifyEd25519Signature, SignatureVerification} from "../e2ee/common";
|
||||
import {BaseObservableValue, RetainedObservableValue} from "../../observable/value";
|
||||
import {pkSign} from "./common";
|
||||
import {SASVerification} from "./SAS/SASVerification";
|
||||
import {ToDeviceChannel} from "./SAS/channel/Channel";
|
||||
import {ToDeviceChannel} from "./SAS/channel/ToDeviceChannel";
|
||||
import {RoomChannel} from "./SAS/channel/RoomChannel";
|
||||
import {VerificationEventType} from "./SAS/channel/types";
|
||||
import {ObservableMap} from "../../observable/map";
|
||||
import {SASRequest} from "./SAS/SASRequest";
|
||||
@ -31,6 +32,8 @@ import type {Account} from "../e2ee/Account";
|
||||
import type {ILogItem} from "../../logging/types";
|
||||
import type {DeviceMessageHandler} from "../DeviceMessageHandler.js";
|
||||
import type {SignedValue, DeviceKey} from "../e2ee/common";
|
||||
import type {Room} from "../room/Room.js";
|
||||
import type {IChannel} from "./SAS/channel/IChannel";
|
||||
import type * as OlmNamespace from "@matrix-org/olm";
|
||||
|
||||
type Olm = typeof OlmNamespace;
|
||||
@ -78,6 +81,12 @@ enum MSKVerification {
|
||||
Valid
|
||||
}
|
||||
|
||||
export interface IVerificationMethod {
|
||||
verify(): Promise<boolean>;
|
||||
otherDeviceId: string;
|
||||
otherUserId: string;
|
||||
}
|
||||
|
||||
export class CrossSigning {
|
||||
private readonly storage: Storage;
|
||||
private readonly secretFetcher: SecretFetcher;
|
||||
@ -172,21 +181,39 @@ export class CrossSigning {
|
||||
return this._isMasterKeyTrusted;
|
||||
}
|
||||
|
||||
startVerification(requestOrUserId: string | SASRequest, log: ILogItem): SASVerification | undefined {
|
||||
startVerification(requestOrUserId: SASRequest, logOrRoom: ILogItem): SASVerification | undefined;
|
||||
startVerification(requestOrUserId: string, logOrRoom: ILogItem): SASVerification | undefined;
|
||||
startVerification(requestOrUserId: SASRequest, logOrRoom: Room, _log: ILogItem): SASVerification | undefined;
|
||||
startVerification(requestOrUserId: string, logOrRoom: Room, _log: ILogItem): SASVerification | undefined;
|
||||
startVerification(requestOrUserId: string | SASRequest, logOrRoom: Room | ILogItem, _log?: ILogItem): SASVerification | undefined {
|
||||
const log: ILogItem = _log ?? logOrRoom;
|
||||
if (this.sasVerificationInProgress && !this.sasVerificationInProgress.finished) {
|
||||
log.log({ sasVerificationAlreadyInProgress: true });
|
||||
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,
|
||||
clock: this.platform.clock,
|
||||
deviceMessageHandler: this.deviceMessageHandler,
|
||||
ourUserDeviceId: this.deviceId,
|
||||
log
|
||||
}, startingMessage);
|
||||
let channel: IChannel;
|
||||
if (otherUserId === this.ownUserId) {
|
||||
channel = new ToDeviceChannel({
|
||||
deviceTracker: this.deviceTracker,
|
||||
hsApi: this.hsApi,
|
||||
otherUserId,
|
||||
clock: this.platform.clock,
|
||||
deviceMessageHandler: this.deviceMessageHandler,
|
||||
ourUserDeviceId: this.deviceId,
|
||||
log
|
||||
}, startingMessage);
|
||||
}
|
||||
else {
|
||||
channel = new RoomChannel({
|
||||
room: logOrRoom,
|
||||
otherUserId,
|
||||
ourUserId: this.ownUserId,
|
||||
ourUserDeviceId: this.deviceId,
|
||||
log,
|
||||
}, startingMessage);
|
||||
}
|
||||
|
||||
this.sasVerificationInProgress = new SASVerification({
|
||||
olm: this.olm,
|
||||
@ -200,7 +227,6 @@ export class CrossSigning {
|
||||
deviceTracker: this.deviceTracker,
|
||||
hsApi: this.hsApi,
|
||||
clock: this.platform.clock,
|
||||
crossSigning: this,
|
||||
});
|
||||
return this.sasVerificationInProgress;
|
||||
}
|
||||
@ -248,13 +274,19 @@ export class CrossSigning {
|
||||
}
|
||||
|
||||
/** @return the signed device key for the given device id */
|
||||
async signDevice(deviceId: string, log: ILogItem): Promise<DeviceKey | undefined> {
|
||||
async signDevice(verification: IVerificationMethod, log: ILogItem): Promise<DeviceKey | undefined> {
|
||||
return log.wrap("CrossSigning.signDevice", async log => {
|
||||
log.set("id", deviceId);
|
||||
if (!this._isMasterKeyTrusted) {
|
||||
log.set("mskNotTrusted", true);
|
||||
return;
|
||||
}
|
||||
const shouldSign = await verification.verify();
|
||||
log.set("shouldSign", shouldSign);
|
||||
if (!shouldSign) {
|
||||
return;
|
||||
}
|
||||
const deviceId = verification.otherDeviceId;
|
||||
log.set("id", deviceId);
|
||||
const keyToSign = await this.deviceTracker.deviceForId(this.ownUserId, deviceId, this.hsApi, log);
|
||||
if (!keyToSign) {
|
||||
return undefined;
|
||||
@ -265,8 +297,9 @@ export class CrossSigning {
|
||||
}
|
||||
|
||||
/** @return the signed MSK for the given user id */
|
||||
async signUser(userId: string, log: ILogItem): Promise<CrossSigningKey | undefined> {
|
||||
async signUser(verification: IVerificationMethod, log: ILogItem): Promise<CrossSigningKey | undefined> {
|
||||
return log.wrap("CrossSigning.signUser", async log => {
|
||||
const userId = verification.otherUserId;
|
||||
log.set("id", userId);
|
||||
if (!this._isMasterKeyTrusted) {
|
||||
log.set("mskNotTrusted", true);
|
||||
@ -276,6 +309,11 @@ export class CrossSigning {
|
||||
if (userId === this.ownUserId) {
|
||||
return;
|
||||
}
|
||||
const shouldSign = await verification.verify();
|
||||
log.set("shouldSign", shouldSign);
|
||||
if (!shouldSign) {
|
||||
return;
|
||||
}
|
||||
const keyToSign = await this.deviceTracker.getCrossSigningKeyForUser(userId, KeyUsage.Master, this.hsApi, log);
|
||||
if (!keyToSign) {
|
||||
return;
|
||||
|
@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type {CrossSigning} from "../CrossSigning";
|
||||
import type {Room} from "../../room/Room.js";
|
||||
import type {ILogItem} from "../../../logging/types";
|
||||
|
||||
export class SASRequest {
|
||||
constructor(public readonly startingMessage: any) {}
|
||||
|
||||
@ -26,6 +30,11 @@ export class SASRequest {
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.startingMessage.content.transaction_id;
|
||||
return this.startingMessage.content.transaction_id ?? this.startingMessage.eventId;
|
||||
}
|
||||
|
||||
async reject(crossSigning: CrossSigning, room: Room, log: ILogItem): Promise<void> {
|
||||
const sas = crossSigning.startVerification(this, room, log);
|
||||
await sas?.abort();
|
||||
}
|
||||
}
|
||||
|
@ -19,17 +19,17 @@ 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 type {IChannel} from "./channel/Channel";
|
||||
import type {IChannel} from "./channel/IChannel";
|
||||
import type {HomeServerApi} from "../../net/HomeServerApi";
|
||||
import type {Timeout} from "../../../platform/types/types";
|
||||
import type {Clock} from "../../../platform/web/dom/Clock.js";
|
||||
import type {IVerificationMethod} from "../CrossSigning";
|
||||
import {CancelReason, VerificationEventType} from "./channel/types";
|
||||
import {SendReadyStage} from "./stages/SendReadyStage";
|
||||
import {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||
import {VerificationCancelledError} from "./VerificationCancelledError";
|
||||
import {EventEmitter} from "../../../utils/EventEmitter";
|
||||
import {SASProgressEvents} from "./types";
|
||||
import type {CrossSigning} from "../CrossSigning";
|
||||
|
||||
type Olm = typeof OlmNamespace;
|
||||
|
||||
@ -45,15 +45,16 @@ type Options = {
|
||||
deviceTracker: DeviceTracker;
|
||||
hsApi: HomeServerApi;
|
||||
clock: Clock;
|
||||
crossSigning: CrossSigning
|
||||
}
|
||||
|
||||
export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
export class SASVerification extends EventEmitter<SASProgressEvents> implements IVerificationMethod {
|
||||
private startStage: BaseSASVerificationStage;
|
||||
private olmSas: Olm.SAS;
|
||||
public finished: boolean = false;
|
||||
public readonly channel: IChannel;
|
||||
private timeout: Timeout;
|
||||
public otherUserId: string;
|
||||
private ourUserId: string;
|
||||
|
||||
constructor(options: Options) {
|
||||
super();
|
||||
@ -61,6 +62,8 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
const olmSas = new olm.SAS();
|
||||
this.olmSas = olmSas;
|
||||
this.channel = channel;
|
||||
this.otherUserId = options.otherUserId;
|
||||
this.ourUserId = options.ourUserId;
|
||||
this.setupCancelAfterTimeout(clock);
|
||||
const stageOptions = {...options, olmSas, eventEmitter: this};
|
||||
if (channel.getReceivedMessage(VerificationEventType.Start)) {
|
||||
@ -74,7 +77,7 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
private async setupCancelAfterTimeout(clock: Clock) {
|
||||
private async setupCancelAfterTimeout(clock: Clock): Promise<void> {
|
||||
try {
|
||||
const tenMinutes = 10 * 60 * 1000;
|
||||
this.timeout = clock.createTimeout(tenMinutes);
|
||||
@ -86,11 +89,13 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
async abort() {
|
||||
async abort(): Promise<void> {
|
||||
await this.channel.cancelVerification(CancelReason.UserCancelled);
|
||||
this.finished = true;
|
||||
}
|
||||
|
||||
async start() {
|
||||
async verify(): Promise<boolean> {
|
||||
let success = true;
|
||||
try {
|
||||
let stage = this.startStage;
|
||||
do {
|
||||
@ -102,6 +107,7 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
if (!(e instanceof VerificationCancelledError)) {
|
||||
throw e;
|
||||
}
|
||||
success = false;
|
||||
}
|
||||
finally {
|
||||
if (this.channel.isCancelled) {
|
||||
@ -111,6 +117,15 @@ export class SASVerification extends EventEmitter<SASProgressEvents> {
|
||||
this.timeout.abort();
|
||||
this.finished = true;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
get otherDeviceId(): string {
|
||||
return this.channel.otherUserDeviceId;
|
||||
}
|
||||
|
||||
get isCrossSigningAnotherUser(): boolean {
|
||||
return !(this.otherUserId === this.ourUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +204,6 @@ export function tests() {
|
||||
olm,
|
||||
startingMessage,
|
||||
);
|
||||
const crossSigning = new MockCrossSigning() as unknown as CrossSigning;
|
||||
const clock = new MockClock();
|
||||
const logger = new NullLogger();
|
||||
return logger.run("log", (log) => {
|
||||
@ -207,7 +221,6 @@ export function tests() {
|
||||
ourUserId,
|
||||
ourUserDeviceId: ourDeviceId,
|
||||
log,
|
||||
crossSigning
|
||||
});
|
||||
// @ts-ignore
|
||||
channel.setOlmSas(sas.olmSas);
|
||||
@ -218,16 +231,6 @@ export function tests() {
|
||||
});
|
||||
}
|
||||
|
||||
class MockCrossSigning {
|
||||
signDevice(deviceId: string, log: ILogItem) {
|
||||
return Promise.resolve({}); // device keys, means signing succeeded
|
||||
}
|
||||
|
||||
signUser(userId: string, log: ILogItem) {
|
||||
return Promise.resolve({}); // cross-signing keys, means signing succeeded
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"Order of stages created matches expected order when I sent request, they sent start": async (assert) => {
|
||||
const ourDeviceId = "ILQHOACESQ";
|
||||
@ -247,7 +250,7 @@ export function tests() {
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
@ -289,7 +292,7 @@ export function tests() {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
@ -326,7 +329,7 @@ export function tests() {
|
||||
receivedMessages,
|
||||
startingMessage,
|
||||
);
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SelectVerificationMethodStage,
|
||||
SendAcceptVerificationStage,
|
||||
@ -364,7 +367,7 @@ export function tests() {
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
@ -408,7 +411,7 @@ export function tests() {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
@ -448,7 +451,7 @@ export function tests() {
|
||||
receivedMessages,
|
||||
startingMessage,
|
||||
);
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SelectVerificationMethodStage,
|
||||
SendAcceptVerificationStage,
|
||||
@ -494,7 +497,7 @@ export function tests() {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SelectVerificationMethodStage,
|
||||
SendKeyStage,
|
||||
@ -537,7 +540,7 @@ export function tests() {
|
||||
await stage?.selectEmojiMethod(log);
|
||||
});
|
||||
});
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
@ -575,7 +578,7 @@ export function tests() {
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
await sas.start();
|
||||
await sas.verify();
|
||||
const expectedOrder = [
|
||||
SendRequestVerificationStage,
|
||||
SelectVerificationMethodStage,
|
||||
@ -613,7 +616,7 @@ export function tests() {
|
||||
txnId,
|
||||
receivedMessages
|
||||
);
|
||||
const promise = sas.start();
|
||||
const promise = sas.verify();
|
||||
clock.elapse(10 * 60 * 1000);
|
||||
try {
|
||||
await promise;
|
||||
@ -643,7 +646,7 @@ export function tests() {
|
||||
receivedMessages
|
||||
);
|
||||
try {
|
||||
await sas.start()
|
||||
await sas.verify()
|
||||
}
|
||||
catch (e) {
|
||||
assert.strictEqual(e instanceof VerificationCancelledError, true);
|
||||
|
48
src/matrix/verification/SAS/channel/IChannel.ts
Normal file
48
src/matrix/verification/SAS/channel/IChannel.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
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 type {ILogItem} from "../../../../logging/types";
|
||||
import {CancelReason, VerificationEventType} from "./types";
|
||||
|
||||
export const messageFromErrorType = {
|
||||
[CancelReason.UserCancelled]: "User declined",
|
||||
[CancelReason.InvalidMessage]: "Invalid Message.",
|
||||
[CancelReason.KeyMismatch]: "Key Mismatch.",
|
||||
[CancelReason.OtherDeviceAccepted]: "Another device has accepted this request.",
|
||||
[CancelReason.TimedOut]: "Timed Out",
|
||||
[CancelReason.UnexpectedMessage]: "Unexpected Message.",
|
||||
[CancelReason.UnknownMethod]: "Unknown method.",
|
||||
[CancelReason.UnknownTransaction]: "Unknown Transaction.",
|
||||
[CancelReason.UserMismatch]: "User Mismatch",
|
||||
[CancelReason.MismatchedCommitment]: "Hash commitment does not match.",
|
||||
[CancelReason.MismatchedSAS]: "Emoji/decimal does not match.",
|
||||
}
|
||||
|
||||
export interface IChannel {
|
||||
send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void>;
|
||||
waitForEvent(eventType: VerificationEventType): Promise<any>;
|
||||
getSentMessage(event: VerificationEventType): any;
|
||||
getReceivedMessage(event: VerificationEventType): any;
|
||||
setStartMessage(content: any): void;
|
||||
cancelVerification(cancellationType: CancelReason): Promise<void>;
|
||||
acceptMessage: any;
|
||||
startMessage: any;
|
||||
initiatedByUs: boolean;
|
||||
isCancelled: boolean;
|
||||
cancellation?: { code: CancelReason, cancelledByUs: boolean };
|
||||
id: string;
|
||||
otherUserDeviceId: string;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import type {ILogItem} from "../../../../lib";
|
||||
import {createCalculateMAC} from "../mac";
|
||||
import {VerificationCancelledError} from "../VerificationCancelledError";
|
||||
import {IChannel} from "./Channel";
|
||||
import {IChannel} from "./IChannel";
|
||||
import {CancelReason, VerificationEventType} from "./types";
|
||||
import {getKeyEd25519Key} from "../../CrossSigning";
|
||||
import {getDeviceEd25519Key} from "../../../e2ee/common";
|
||||
|
243
src/matrix/verification/SAS/channel/RoomChannel.ts
Normal file
243
src/matrix/verification/SAS/channel/RoomChannel.ts
Normal file
@ -0,0 +1,243 @@
|
||||
/*
|
||||
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 type {ILogItem} from "../../../../logging/types";
|
||||
import type {IChannel} from "./IChannel";
|
||||
import type {Room} from "../../../room/Room.js";
|
||||
import type {EventEntry} from "../../../room/timeline/entries/EventEntry.js";
|
||||
import {messageFromErrorType} from "./IChannel";
|
||||
import {CancelReason, VerificationEventType} from "./types";
|
||||
import {Disposables} from "../../../../utils/Disposables";
|
||||
import {VerificationCancelledError} from "../VerificationCancelledError";
|
||||
import {Deferred} from "../../../../utils/Deferred";
|
||||
import {getRelatedEventId, createReference} from "../../../room/timeline/relations.js";
|
||||
|
||||
type Options = {
|
||||
otherUserId: string;
|
||||
ourUserId: string;
|
||||
log: ILogItem;
|
||||
ourUserDeviceId: string;
|
||||
room: Room;
|
||||
}
|
||||
|
||||
export class RoomChannel extends Disposables implements IChannel {
|
||||
private ourDeviceId: string;
|
||||
private readonly otherUserId: string;
|
||||
private readonly sentMessages: Map<VerificationEventType, any> = new Map();
|
||||
private readonly receivedMessages: Map<VerificationEventType, any> = new Map();
|
||||
private readonly waitMap: Map<string, Deferred<any>> = new Map();
|
||||
private readonly log: ILogItem;
|
||||
private readonly room: Room;
|
||||
private readonly ourUserId: string;
|
||||
public otherUserDeviceId: string;
|
||||
public startMessage: any;
|
||||
/**
|
||||
* This is the event-id of the starting message (request/start)
|
||||
*/
|
||||
public id: string;
|
||||
private _initiatedByUs: boolean;
|
||||
private _cancellation?: { code: CancelReason, cancelledByUs: boolean };
|
||||
|
||||
/**
|
||||
*
|
||||
* @param startingMessage Create the channel with existing message in the receivedMessage buffer
|
||||
*/
|
||||
constructor(options: Options, startingMessage?: any) {
|
||||
super();
|
||||
this.otherUserId = options.otherUserId;
|
||||
this.ourUserId = options.ourUserId;
|
||||
this.ourDeviceId = options.ourUserDeviceId;
|
||||
this.log = options.log;
|
||||
this.room = options.room;
|
||||
this.subscribeToTimeline();
|
||||
this.track(() => {
|
||||
this.waitMap.forEach((value) => {
|
||||
value.reject(new VerificationCancelledError());
|
||||
});
|
||||
});
|
||||
// Copy over request message
|
||||
if (startingMessage) {
|
||||
/**
|
||||
* startingMessage may be the ready message or the start message.
|
||||
*/
|
||||
this.id = startingMessage.id;
|
||||
const type = startingMessage.content?.msgtype ?? startingMessage.eventType;
|
||||
this.receivedMessages.set(type, startingMessage);
|
||||
this.otherUserDeviceId = startingMessage.content.from_device;
|
||||
}
|
||||
}
|
||||
|
||||
private async subscribeToTimeline() {
|
||||
const timeline = await this.room.openTimeline();
|
||||
this.track(() => timeline.release());
|
||||
this.track(
|
||||
timeline.entries.subscribe({
|
||||
onAdd: async (_, entry: EventEntry) => {
|
||||
this.handleRoomMessage(entry);
|
||||
},
|
||||
onRemove: () => { /** noop */ },
|
||||
onUpdate: () => { /** noop */ },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get cancellation(): IChannel["cancellation"] {
|
||||
return this._cancellation;
|
||||
};
|
||||
|
||||
get isCancelled(): boolean {
|
||||
return !!this._cancellation;
|
||||
}
|
||||
|
||||
async send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void> {
|
||||
await log.wrap("RoomChannel.send", async () => {
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
if (eventType === VerificationEventType.Request) {
|
||||
// Handle this case specially
|
||||
await this.handleRequestEventSpecially(eventType, content, log);
|
||||
return;
|
||||
}
|
||||
Object.assign(content, createReference(this.id));
|
||||
await this.room.sendEvent(eventType, content, undefined, log);
|
||||
this.sentMessages.set(eventType, {content});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRequestEventSpecially(eventType: VerificationEventType, content: any, log: ILogItem) {
|
||||
await log.wrap("RoomChannel.handleRequestEventSpecially", async () => {
|
||||
Object.assign(content, {
|
||||
body: `${this.otherUserId} is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.`,
|
||||
msgtype: VerificationEventType.Request,
|
||||
to: this.otherUserId,
|
||||
});
|
||||
const pendingEvent = await this.room.sendEvent("m.room.message", content, undefined, log);
|
||||
this.id = await pendingEvent.getRemoteId();
|
||||
this.sentMessages.set(eventType, {content});
|
||||
});
|
||||
}
|
||||
|
||||
getReceivedMessage(event: VerificationEventType) {
|
||||
return this.receivedMessages.get(event);
|
||||
}
|
||||
|
||||
getSentMessage(event: VerificationEventType) {
|
||||
return this.sentMessages.get(event);
|
||||
}
|
||||
|
||||
get acceptMessage(): any {
|
||||
return this.receivedMessages.get(VerificationEventType.Accept) ??
|
||||
this.sentMessages.get(VerificationEventType.Accept);
|
||||
}
|
||||
|
||||
private async handleRoomMessage(entry: EventEntry) {
|
||||
const type = entry.content?.msgtype ?? entry.eventType;
|
||||
if (!type?.startsWith("m.key.verification") ||
|
||||
entry.sender === this.ourUserId ||
|
||||
entry.isLoadedFromStorage) {
|
||||
return;
|
||||
}
|
||||
console.log("entry", entry);
|
||||
await this.log.wrap("RoomChannel.handleRoomMessage", async (log) => {
|
||||
if (!this.id) {
|
||||
throw new Error("Couldn't find event-id of request message!");
|
||||
}
|
||||
if (getRelatedEventId(entry.event) !== this.id) {
|
||||
/**
|
||||
* When a device receives an unknown transaction_id, it should send an appropriate
|
||||
* m.key.verification.cancel message to the other device indicating as such.
|
||||
* This does not apply for inbound m.key.verification.start or m.key.verification.cancel messages.
|
||||
*/
|
||||
console.log("Received entry with unknown transaction id: ", entry);
|
||||
await this.cancelVerification(CancelReason.UnknownTransaction);
|
||||
return;
|
||||
}
|
||||
this.resolveAnyWaits(entry);
|
||||
this.receivedMessages.set(entry.eventType, entry);
|
||||
if (entry.eventType === VerificationEventType.Ready) {
|
||||
const fromDevice = entry.content.from_device;
|
||||
this.otherUserDeviceId = fromDevice;
|
||||
return;
|
||||
}
|
||||
if (entry.eventType === VerificationEventType.Cancel) {
|
||||
this._cancellation = { code: entry.content.code, cancelledByUs: false };
|
||||
this.dispose();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async cancelVerification(cancellationType: CancelReason) {
|
||||
await this.log.wrap("RoomChannel.cancelVerification", async log => {
|
||||
log.log({ reason: messageFromErrorType[cancellationType] });
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
const content = {
|
||||
code: cancellationType,
|
||||
reason: messageFromErrorType[cancellationType],
|
||||
}
|
||||
await this.send(VerificationEventType.Cancel, content, log);
|
||||
this._cancellation = { code: cancellationType, cancelledByUs: true };
|
||||
this.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private resolveAnyWaits(entry: EventEntry) {
|
||||
const { eventType } = entry;
|
||||
const wait = this.waitMap.get(eventType);
|
||||
if (wait) {
|
||||
wait.resolve(entry);
|
||||
this.waitMap.delete(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
waitForEvent(eventType: VerificationEventType): Promise<any> {
|
||||
if (this.isCancelled) {
|
||||
throw new VerificationCancelledError();
|
||||
}
|
||||
// Check if we already received the message
|
||||
const receivedMessage = this.receivedMessages.get(eventType);
|
||||
if (receivedMessage) {
|
||||
return Promise.resolve(receivedMessage);
|
||||
}
|
||||
// Check if we're already waiting for this message
|
||||
const existingWait = this.waitMap.get(eventType);
|
||||
if (existingWait) {
|
||||
return existingWait.promise;
|
||||
}
|
||||
const deferred = new Deferred();
|
||||
this.waitMap.set(eventType, deferred);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
setStartMessage(entry) {
|
||||
if (!entry.content["m.relates_to"]) {
|
||||
const clone = entry.clone();
|
||||
clone.content["m.relates_to"] = clone.event.content["m.relates_to"];
|
||||
this.startMessage = clone;
|
||||
}
|
||||
else {
|
||||
this.startMessage = entry;
|
||||
}
|
||||
this._initiatedByUs = entry.content.from_device === this.ourDeviceId;
|
||||
}
|
||||
|
||||
get initiatedByUs(): boolean {
|
||||
return this._initiatedByUs;
|
||||
};
|
||||
}
|
@ -19,42 +19,14 @@ import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js";
|
||||
import type {ILogItem} from "../../../../logging/types";
|
||||
import type {Clock} from "../../../../platform/web/dom/Clock.js";
|
||||
import type {DeviceMessageHandler} from "../../../DeviceMessageHandler.js";
|
||||
import type {IChannel} from "./IChannel";
|
||||
import {messageFromErrorType} from "./IChannel";
|
||||
import {makeTxnId} from "../../../common.js";
|
||||
import {CancelReason, VerificationEventType} from "./types";
|
||||
import {Disposables} from "../../../../utils/Disposables";
|
||||
import {VerificationCancelledError} from "../VerificationCancelledError";
|
||||
import {Deferred} from "../../../../utils/Deferred";
|
||||
|
||||
const messageFromErrorType = {
|
||||
[CancelReason.UserCancelled]: "User declined",
|
||||
[CancelReason.InvalidMessage]: "Invalid Message.",
|
||||
[CancelReason.KeyMismatch]: "Key Mismatch.",
|
||||
[CancelReason.OtherDeviceAccepted]: "Another device has accepted this request.",
|
||||
[CancelReason.TimedOut]: "Timed Out",
|
||||
[CancelReason.UnexpectedMessage]: "Unexpected Message.",
|
||||
[CancelReason.UnknownMethod]: "Unknown method.",
|
||||
[CancelReason.UnknownTransaction]: "Unknown Transaction.",
|
||||
[CancelReason.UserMismatch]: "User Mismatch",
|
||||
[CancelReason.MismatchedCommitment]: "Hash commitment does not match.",
|
||||
[CancelReason.MismatchedSAS]: "Emoji/decimal does not match.",
|
||||
}
|
||||
|
||||
export interface IChannel {
|
||||
send(eventType: VerificationEventType, content: any, log: ILogItem): Promise<void>;
|
||||
waitForEvent(eventType: VerificationEventType): Promise<any>;
|
||||
getSentMessage(event: VerificationEventType): any;
|
||||
getReceivedMessage(event: VerificationEventType): any;
|
||||
setStartMessage(content: any): void;
|
||||
cancelVerification(cancellationType: CancelReason): Promise<void>;
|
||||
acceptMessage: any;
|
||||
startMessage: any;
|
||||
initiatedByUs: boolean;
|
||||
isCancelled: boolean;
|
||||
cancellation?: { code: CancelReason, cancelledByUs: boolean };
|
||||
id: string;
|
||||
otherUserDeviceId: string;
|
||||
}
|
||||
|
||||
type Options = {
|
||||
hsApi: HomeServerApi;
|
||||
deviceTracker: DeviceTracker;
|
@ -16,8 +16,7 @@ limitations under the License.
|
||||
import type {ILogItem} from "../../../../logging/types";
|
||||
import type {Account} from "../../../e2ee/Account.js";
|
||||
import type {DeviceTracker} from "../../../e2ee/DeviceTracker.js";
|
||||
import type {CrossSigning} from "../../CrossSigning";
|
||||
import {IChannel} from "../channel/Channel";
|
||||
import {IChannel} from "../channel/IChannel";
|
||||
import {HomeServerApi} from "../../../net/HomeServerApi";
|
||||
import {SASProgressEvents} from "../types";
|
||||
import {EventEmitter} from "../../../../utils/EventEmitter";
|
||||
@ -34,7 +33,6 @@ export type Options = {
|
||||
deviceTracker: DeviceTracker;
|
||||
hsApi: HomeServerApi;
|
||||
eventEmitter: EventEmitter<SASProgressEvents>
|
||||
crossSigning: CrossSigning
|
||||
}
|
||||
|
||||
export abstract class BaseSASVerificationStage {
|
||||
|
@ -75,7 +75,9 @@ export class CalculateSASStage extends BaseSASVerificationStage {
|
||||
const sasBytes = this.generateSASBytes();
|
||||
this.emoji = generateEmojiSas(Array.from(sasBytes));
|
||||
this.eventEmitter.emit("EmojiGenerated", this);
|
||||
await emojiConfirmationPromise;
|
||||
const cancellationReceived = this.channel.waitForEvent(VerificationEventType.Cancel);
|
||||
// Don't get stuck on waiting for user input!
|
||||
await Promise.race([emojiConfirmationPromise, cancellationReceived]);
|
||||
this.setNextStage(new SendMacStage(this.options));
|
||||
});
|
||||
}
|
||||
|
@ -68,11 +68,8 @@ export class VerifyMacStage extends BaseSASVerificationStage {
|
||||
const deviceIdOrMSK = keyId.split(":", 2)[1];
|
||||
const device = await this.deviceTracker.deviceForId(userId, deviceIdOrMSK, this.hsApi, log);
|
||||
if (device) {
|
||||
if (verifier(keyId, getDeviceEd25519Key(device), keyInfo)) {
|
||||
await log.wrap("signing device", async log => {
|
||||
const signedKey = await this.options.crossSigning.signDevice(device.device_id, log);
|
||||
log.set("success", !!signedKey);
|
||||
});
|
||||
if (!verifier(keyId, getDeviceEd25519Key(device), keyInfo)) {
|
||||
throw new Error(`MAC verification failed for key ${keyInfo}`);
|
||||
}
|
||||
} else {
|
||||
// If we were not able to find the device, then deviceIdOrMSK is actually the MSK!
|
||||
@ -82,11 +79,8 @@ export class VerifyMacStage extends BaseSASVerificationStage {
|
||||
throw new Error("Fetching MSK for user failed!");
|
||||
}
|
||||
const masterKey = getKeyEd25519Key(key);
|
||||
if(masterKey && verifier(keyId, masterKey, keyInfo)) {
|
||||
await log.wrap("signing user", async log => {
|
||||
const signedKey = await this.options.crossSigning.signUser(userId, log);
|
||||
log.set("success", !!signedKey);
|
||||
});
|
||||
if(!(masterKey && verifier(keyId, masterKey, keyInfo))) {
|
||||
throw new Error(`MAC verification failed for key ${keyInfo}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ 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 type {IChannel} from "./channel/Channel";
|
||||
import type {IChannel} from "./channel/IChannel";
|
||||
import type {CalculateSASStage} from "./stages/CalculateSASStage";
|
||||
import type {SelectVerificationMethodStage} from "./stages/SelectVerificationMethodStage";
|
||||
|
||||
|
@ -1399,6 +1399,10 @@ button.RoomDetailsView_row::after {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.VerifyEmojisView {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.VerificationCompleteView,
|
||||
.DeviceVerificationView,
|
||||
.SelectMethodView {
|
||||
@ -1406,6 +1410,14 @@ button.RoomDetailsView_row::after {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.SelectMethodView > div,
|
||||
.SelectMethodView__heading,
|
||||
.SelectMethodView__title {
|
||||
width: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.VerificationCompleteView__heading,
|
||||
@ -1428,6 +1440,15 @@ button.RoomDetailsView_row::after {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.VerificationCancelledView__title,
|
||||
.VerificationCompleteView__title,
|
||||
.VerifyEmojisView__title,
|
||||
.SelectMethodView__title,
|
||||
.WaitingForOtherUserView__title {
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.VerificationCompleteView__title,
|
||||
.VerifyEmojisView__title,
|
||||
.SelectMethodView__title,
|
||||
@ -1457,6 +1478,7 @@ button.RoomDetailsView_row::after {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.EmojiContainer__emoji {
|
||||
@ -1485,3 +1507,66 @@ button.RoomDetailsView_row::after {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.SelectMethodView__name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.VerificationInProgressTileView,
|
||||
.VerificationCompletedTileView,
|
||||
.VerificationCancelledTileView,
|
||||
.VerificationReadyTileView {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.VerificationTileView__actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.VerificationTileView__description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.4rem;
|
||||
color: var(--text-color);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.VerificationInProgressTileView,
|
||||
.VerificationCompletedTileView,
|
||||
.VerificationCancelledTileView,
|
||||
.VerificationReadyTileView {
|
||||
background: var(--background-color-primary--darker-5);
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.VerificationTileView {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
.VerificationInProgressTileView .VerificationTileView__shield,
|
||||
.VerificationReadyTileView .VerificationTileView__shield {
|
||||
background: url("./icons/e2ee-normal.svg?primary=background-color-secondary--darker-40");
|
||||
}
|
||||
|
||||
.VerificationCompletedTileView .VerificationTileView__shield {
|
||||
background: url("./icons/e2ee-normal.svg?primary=accent-color");
|
||||
}
|
||||
|
||||
.VerificationTileView__shield {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
}
|
||||
|
@ -56,6 +56,9 @@ export class MemberDetailsView extends TemplateView {
|
||||
t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`)
|
||||
];
|
||||
if (vm.features.crossSigning) {
|
||||
if (vm.canVerifyUser) {
|
||||
options.push(t.button({ className: "text", onClick: () => vm.verifyUser() }, vm.i18n`Verify`));
|
||||
}
|
||||
const onClick = () => {
|
||||
if (confirm("You don't want to do this with any account but a test account. This will cross-sign this user without verifying their keys first. You won't be able to undo this apart from resetting your cross-signing keys.")) {
|
||||
vm.signUser();
|
||||
|
@ -19,6 +19,7 @@ import {RoomDetailsView} from "./RoomDetailsView.js";
|
||||
import {MemberListView} from "./MemberListView.js";
|
||||
import {LoadingView} from "../../general/LoadingView.js";
|
||||
import {MemberDetailsView} from "./MemberDetailsView.js";
|
||||
import {DeviceVerificationView} from "../verification/DeviceVerificationView";
|
||||
|
||||
export class RightPanelView extends TemplateView {
|
||||
render(t) {
|
||||
@ -39,6 +40,8 @@ export class RightPanelView extends TemplateView {
|
||||
return new MemberListView(vm);
|
||||
case "member-details":
|
||||
return new MemberDetailsView(vm);
|
||||
case "verification":
|
||||
return new DeviceVerificationView(vm);
|
||||
default:
|
||||
return new LoadingView();
|
||||
}
|
||||
@ -53,7 +56,7 @@ class ButtonsView extends TemplateView {
|
||||
className: {
|
||||
"back": true,
|
||||
"button-utility": true,
|
||||
"hide": !vm.activeViewModel.shouldShowBackButton
|
||||
"hide": (vm) => !vm.activeViewModel.shouldShowBackButton
|
||||
}, onClick: () => vm.showPreviousPanel()}),
|
||||
t.button({className: "close button-utility", onClick: () => vm.closePanel()})
|
||||
]);
|
||||
|
@ -15,31 +15,28 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {ListView} from "../../general/ListView";
|
||||
import type {IView} from "../../general/types";
|
||||
import {TemplateView, Builder} from "../../general/TemplateView";
|
||||
import {IObservableValue} from "../../general/BaseUpdateView";
|
||||
import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
|
||||
import {AnnouncementView} from "./timeline/AnnouncementView.js";
|
||||
import {RedactedView} from "./timeline/RedactedView.js";
|
||||
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
||||
import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList";
|
||||
import type {IView} from "../../general/types";
|
||||
import type {ITile} from "../../../../../domain/session/room/timeline/tiles/ITile";
|
||||
|
||||
export interface TileView extends IView {
|
||||
readonly value: SimpleTile;
|
||||
readonly value: ITile;
|
||||
onClick(event: UIEvent);
|
||||
}
|
||||
export type TileViewConstructor = new (
|
||||
tile: SimpleTile,
|
||||
tile: ITile,
|
||||
viewClassForTile: ViewClassForEntryFn,
|
||||
renderFlags?: { reply?: boolean, interactive?: boolean }
|
||||
) => TileView;
|
||||
export type ViewClassForEntryFn = (tile: SimpleTile) => TileViewConstructor;
|
||||
export type ViewClassForEntryFn = (tile: ITile) => TileViewConstructor;
|
||||
|
||||
//import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
|
||||
export interface TimelineViewModel extends IObservableValue {
|
||||
showJumpDown: boolean;
|
||||
tiles: ObservableList<SimpleTile>;
|
||||
setVisibleTileRange(start?: SimpleTile, end?: SimpleTile);
|
||||
tiles: ObservableList<ITile>;
|
||||
setVisibleTileRange(start?: ITile, end?: ITile);
|
||||
}
|
||||
|
||||
function bottom(node: HTMLElement): number {
|
||||
@ -184,11 +181,11 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
||||
}
|
||||
}
|
||||
|
||||
class TilesListView extends ListView<SimpleTile, TileView> {
|
||||
class TilesListView extends ListView<ITile, TileView> {
|
||||
|
||||
private onChanged: () => void;
|
||||
|
||||
constructor(tiles: ObservableList<SimpleTile>, onChanged: () => void, private readonly viewClassForTile: ViewClassForEntryFn) {
|
||||
constructor(tiles: ObservableList<ITile>, onChanged: () => void, private readonly viewClassForTile: ViewClassForEntryFn) {
|
||||
super({
|
||||
list: tiles,
|
||||
onItemClick: (tileView, evt) => tileView.onClick(evt),
|
||||
@ -204,7 +201,7 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
||||
this.onChanged();
|
||||
}
|
||||
|
||||
onUpdate(index: number, value: SimpleTile, param: any) {
|
||||
onUpdate(index: number, value: ITile, param: any) {
|
||||
if (param === "shape") {
|
||||
const ExpectedClass = this.viewClassForTile(value);
|
||||
const child = this.getChildInstanceByIndex(index);
|
||||
@ -220,17 +217,17 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
||||
this.onChanged();
|
||||
}
|
||||
|
||||
onAdd(idx: number, value: SimpleTile) {
|
||||
onAdd(idx: number, value: ITile) {
|
||||
super.onAdd(idx, value);
|
||||
this.onChanged();
|
||||
}
|
||||
|
||||
onRemove(idx: number, value: SimpleTile) {
|
||||
onRemove(idx: number, value: ITile) {
|
||||
super.onRemove(idx, value);
|
||||
this.onChanged();
|
||||
}
|
||||
|
||||
onMove(fromIdx: number, toIdx: number, value: SimpleTile) {
|
||||
onMove(fromIdx: number, toIdx: number, value: ITile) {
|
||||
super.onMove(fromIdx, toIdx, value);
|
||||
this.onChanged();
|
||||
}
|
||||
|
@ -22,11 +22,12 @@ import {LocationView} from "./timeline/LocationView.js";
|
||||
import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
|
||||
import {AnnouncementView} from "./timeline/AnnouncementView.js";
|
||||
import {RedactedView} from "./timeline/RedactedView.js";
|
||||
import {ITile, TileShape} from "../../../../../domain/session/room/timeline/tiles/ITile.js";
|
||||
import {ITile, TileShape} from "../../../../../domain/session/room/timeline/tiles/ITile";
|
||||
import {GapView} from "./timeline/GapView.js";
|
||||
import {CallTileView} from "./timeline/CallTileView";
|
||||
import {DateHeaderView} from "./timeline/DateHeaderView";
|
||||
import type {TileViewConstructor, ViewClassForEntryFn} from "./TimelineView";
|
||||
import {VerificationTileView} from "./timeline/VerificationTileView";
|
||||
import type {TileViewConstructor} from "./TimelineView";
|
||||
|
||||
export function viewClassForTile(vm: ITile): TileViewConstructor {
|
||||
switch (vm.shape) {
|
||||
@ -53,6 +54,8 @@ export function viewClassForTile(vm: ITile): TileViewConstructor {
|
||||
return CallTileView;
|
||||
case TileShape.DateHeader:
|
||||
return DateHeaderView;
|
||||
case TileShape.Verification:
|
||||
return VerificationTileView;
|
||||
default:
|
||||
throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model`);
|
||||
}
|
||||
|
@ -15,15 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {TemplateView} from "../../../general/TemplateView";
|
||||
import {spinner} from "../../../common.js";
|
||||
import type {DateTile} from "../../../../../../domain/session/room/timeline/tiles/DateTile";
|
||||
|
||||
export class DateHeaderView extends TemplateView<DateTile> {
|
||||
// ignore other argument
|
||||
constructor(vm) {
|
||||
super(vm);
|
||||
}
|
||||
|
||||
render(t, vm) {
|
||||
return t.h2({className: "DateHeader"}, t.time({dateTime: vm.machineReadableDate}, vm.relativeDate));
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TemplateView} from "../../../general/TemplateView";
|
||||
import {Status} from "../../../../../../domain/session/room/timeline/tiles/VerificationTile";
|
||||
import {spinner} from "../../../common.js";
|
||||
import type {IView} from "../../../general/types";
|
||||
import type {Builder} from "../../../general/TemplateView";
|
||||
import type {VerificationTile} from "../../../../../../domain/session/room/timeline/tiles/VerificationTile";
|
||||
|
||||
type IClickableView = {
|
||||
onClick: (evt) => void;
|
||||
} & IView;
|
||||
|
||||
export class VerificationTileView extends TemplateView<VerificationTile> {
|
||||
render(t: Builder<VerificationTile>, vm: VerificationTile) {
|
||||
return t.div({ className: "VerificationTileView" },
|
||||
t.mapView(vm => vm.status, (status: Status) => {
|
||||
switch (status) {
|
||||
case Status.Ready:
|
||||
return new VerificationReadyTileView(vm);
|
||||
case Status.Cancelled:
|
||||
return new VerificationCancelledTileView(vm);
|
||||
case Status.Completed:
|
||||
return new VerificationCompletedTileView(vm);
|
||||
case Status.InProgress:
|
||||
return new VerificationInProgressTileView(vm);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onClick(evt) {
|
||||
// Propagate click events to the sub-view
|
||||
this._subViews?.forEach((s: IClickableView) => s.onClick?.(evt));
|
||||
}
|
||||
}
|
||||
|
||||
class VerificationReadyTileView extends TemplateView<VerificationTile> {
|
||||
render(t: Builder<VerificationTile>, vm: VerificationTile) {
|
||||
return t.div({ className: "VerificationReadyTileView" }, [
|
||||
t.div({ className: "VerificationTileView__description" }, [
|
||||
t.div({ className: "VerificationTileView__shield" }),
|
||||
t.div(vm.description)
|
||||
]),
|
||||
t.div({ className: "VerificationTileView__actions" }, [
|
||||
t.button({ className: "VerificationTileView__accept button-action primary" }, "Accept"),
|
||||
t.button({ className: "VerificationTileView__reject button-action secondary" }, "Reject"),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
onClick(evt) {
|
||||
if (evt.target.classList.contains("VerificationTileView__accept")) {
|
||||
this.value.accept();
|
||||
} else if (evt.target.classList.contains("VerificationTileView__reject")) {
|
||||
this.value.reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class VerificationCancelledTileView extends TemplateView<VerificationTile> {
|
||||
render(t: Builder<VerificationTile>, vm: VerificationTile) {
|
||||
return t.div({ className: "VerificationCancelledTileView" }, [
|
||||
t.div({ className: "VerificationTileView__description" },
|
||||
vm.i18n`${vm.isCancelledByUs? "You": vm.sender} cancelled the verification!`),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class VerificationCompletedTileView extends TemplateView<VerificationTile> {
|
||||
render(t: Builder<VerificationTile>, vm: VerificationTile) {
|
||||
return t.div({ className: "VerificationCompletedTileView" }, [
|
||||
t.div({ className: "VerificationTileView__description" }, [
|
||||
t.div({ className: "VerificationTileView__shield" }),
|
||||
t.div(vm.i18n`You verified ${vm.sender}`),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class VerificationInProgressTileView extends TemplateView<VerificationTile> {
|
||||
render(t: Builder<VerificationTile>, vm: VerificationTile) {
|
||||
return t.div({ className: "VerificationInProgressTileView" }, [
|
||||
t.div({ className: "VerificationTileView__description" },
|
||||
vm.i18n`Verification in progress`),
|
||||
t.div({ className: "VerificationTileView__actions" }, [
|
||||
spinner(t)
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
@ -27,11 +27,9 @@ export class SelectMethodView extends TemplateView<SelectMethodViewModel> {
|
||||
}
|
||||
else return t.div([
|
||||
t.div({ className: "SelectMethodView__heading" }, [
|
||||
t.h2( { className: "SelectMethodView__title" }, vm.i18n`Verify device '${vm.deviceName}' by comparing emojis?`),
|
||||
t.h2( { className: "SelectMethodView__title" }, this.getHeading(t, vm)),
|
||||
]),
|
||||
t.p({ className: "SelectMethodView__description" },
|
||||
vm.i18n`You are about to verify your other device by comparing emojis.`
|
||||
),
|
||||
t.p({ className: "SelectMethodView__description" }, this.getSubheading(vm)),
|
||||
t.div({ className: "SelectMethodView__actions" }, [
|
||||
t.button(
|
||||
{
|
||||
@ -59,4 +57,24 @@ export class SelectMethodView extends TemplateView<SelectMethodViewModel> {
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
getHeading(t: Builder<SelectMethodViewModel>, vm: SelectMethodViewModel) {
|
||||
if (vm.isCrossSigningAnotherUser) {
|
||||
return [vm.i18n`Verify user `, t.span({
|
||||
className: "SelectMethodView__name"
|
||||
}, vm.otherUserId), vm.i18n` by comparing emojis?`];
|
||||
} else {
|
||||
return [vm.i18n`Verify device`, t.span({
|
||||
className: "SelectMethodView__name"
|
||||
}, vm.deviceName), vm.i18n` by comparing emojis?`];
|
||||
}
|
||||
}
|
||||
|
||||
getSubheading(vm: SelectMethodViewModel): string {
|
||||
if (vm.isCrossSigningAnotherUser) {
|
||||
return vm.i18n`You are about to verify user (${vm.otherUserId}) by comparing emojis.`;
|
||||
} else {
|
||||
return vm.i18n`You are about to verify your other device (${vm.deviceName}) by comparing emojis.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ export class VerificationCancelledView extends TemplateView<VerificationCancelle
|
||||
"button-action": true,
|
||||
"primary": true,
|
||||
},
|
||||
onclick: () => vm.gotoSettings(),
|
||||
onclick: () => vm.dismiss(),
|
||||
}, "Got it")
|
||||
]),
|
||||
]
|
||||
|
@ -28,16 +28,14 @@ export class VerificationCompleteView extends TemplateView<VerificationCompleteV
|
||||
),
|
||||
]),
|
||||
t.p(
|
||||
{ className: "VerificationCompleteView__description" },
|
||||
vm.i18n`You successfully verified device ${vm.otherDeviceId}`
|
||||
),
|
||||
{ className: "VerificationCompleteView__description" }, vm.verificationSuccessfulMessage),
|
||||
t.div({ className: "VerificationCompleteView__actions" }, [
|
||||
t.button({
|
||||
className: {
|
||||
"button-action": true,
|
||||
"primary": true,
|
||||
},
|
||||
onclick: () => vm.gotoSettings(),
|
||||
onclick: () => vm.dismiss(),
|
||||
}, "Got it")
|
||||
]),
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user