diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index b313ab42..59bb1ccc 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -35,7 +35,7 @@ export type SegmentType = { "members": true; "member": string; "device-verification": string | boolean; - "verification": boolean; + "verification": string | boolean; "join-room": true; }; diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 474d3cbb..fc090aed 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -70,9 +70,12 @@ export class RightPanelViewModel extends ViewModel { } ); this._hookUpdaterToSegment("verification", DeviceVerificationViewModel, () => { + const id = this.navigation.path.get("verification").value; + const request = this._session?.crossSigning.get()?.receivedSASVerifications.get(id); return { session: this._session, room: this._room, + request, } }); } diff --git a/src/domain/session/room/timeline/tiles/ITile.ts b/src/domain/session/room/timeline/tiles/ITile.ts index e47ebcd0..9e25f97a 100644 --- a/src/domain/session/room/timeline/tiles/ITile.ts +++ b/src/domain/session/room/timeline/tiles/ITile.ts @@ -18,6 +18,7 @@ export enum TileShape { Video = "video", DateHeader = "date-header", Call = "call", + Verification = "verification", } // TODO: should we imply inheriting from view model here? diff --git a/src/domain/session/room/timeline/tiles/VerificationTile.ts b/src/domain/session/room/timeline/tiles/VerificationTile.ts new file mode 100644 index 00000000..493706ca --- /dev/null +++ b/src/domain/session/room/timeline/tiles/VerificationTile.ts @@ -0,0 +1,59 @@ +/* +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 type {EventEntry} from "../../../../../matrix/room/timeline/entries/EventEntry.js"; +import type {Options} from "./SimpleTile"; + +export class VerificationTile extends SimpleTile { + private request: SASRequest; + + constructor(entry: EventEntry, options: Options) { + super(entry, options); + this.request = new SASRequest(this.lowerEntry); + } + + 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 { + // create the SasVerification object and call abort() on it + 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); + } +} diff --git a/src/domain/session/room/timeline/tiles/index.ts b/src/domain/session/room/timeline/tiles/index.ts index e86d61cb..348588e0 100644 --- a/src/domain/session/room/timeline/tiles/index.ts +++ b/src/domain/session/room/timeline/tiles/index.ts @@ -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,15 @@ export function tileClassForEntry(entry: TimelineEntry, options: Options): TileC return FileTile; case "m.location": return LocationTile; + case "m.key.verification.request": + const isCrossSigningEnabled = !options.session.features.crossSigning; + const userId = options.session.userId; + if (isCrossSigningEnabled|| + entry.isLoadedFromStorage || + entry.sender === userId) { + return undefined; + } + return VerificationTile as unknown as TileConstructor; default: // unknown msgtype not rendered return undefined; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 23c9cce6..109a55a1 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -132,6 +132,10 @@ export class Session { return this._callHandler; } + get features() { + return this._features; + } + _setupCallHandler() { this._callHandler = new CallHandler({ clock: this._platform.clock, diff --git a/src/matrix/verification/SAS/SASRequest.ts b/src/matrix/verification/SAS/SASRequest.ts index 69bc197a..f623d37f 100644 --- a/src/matrix/verification/SAS/SASRequest.ts +++ b/src/matrix/verification/SAS/SASRequest.ts @@ -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 { + const sas = crossSigning.startVerification(this, room, log); + await sas?.abort(); } } diff --git a/src/matrix/verification/SAS/channel/RoomChannel.ts b/src/matrix/verification/SAS/channel/RoomChannel.ts index 482e3753..dcf60f1c 100644 --- a/src/matrix/verification/SAS/channel/RoomChannel.ts +++ b/src/matrix/verification/SAS/channel/RoomChannel.ts @@ -73,8 +73,9 @@ export class RoomChannel extends Disposables implements IChannel { /** * startingMessage may be the ready message or the start message. */ - this.id = startingMessage.content.transaction_id; - this.receivedMessages.set(startingMessage.type, startingMessage); + this.id = startingMessage.id; + const type = startingMessage.content?.msgtype ?? startingMessage.eventType; + this.receivedMessages.set(type, startingMessage); this.otherUserDeviceId = startingMessage.content.from_device; } } diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 0a27658e..e6adbb92 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1510,3 +1510,31 @@ button.RoomDetailsView_row::after { width: 100%; display: inline-block; } + +.VerificationTileView, +.VerificationTileView__actions { + display: flex; + gap: 16px; +} + +.VerificationTileView__description { + display: flex; + align-items: center; + font-weight: bold; + font-size: 1.4rem; + color: var(--text-color); +} + +.VerificationTileView { + background: var(--background-color-primary--darker-5); + padding: 12px; + box-sizing: border-box; + border-radius: 8px; +} + +.VerificationTileContainer { + display: flex; + justify-content: center; + padding: 16px; + box-sizing: border-box; +} diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts index d435266e..e12f353e 100644 --- a/src/platform/web/ui/session/room/common.ts +++ b/src/platform/web/ui/session/room/common.ts @@ -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 type {TileViewConstructor} from "./TimelineView"; +import {VerificationTileView} from "./timeline/VerificationTileView"; 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`); } diff --git a/src/platform/web/ui/session/room/timeline/VerificationTileView.ts b/src/platform/web/ui/session/room/timeline/VerificationTileView.ts new file mode 100644 index 00000000..f7f5a29f --- /dev/null +++ b/src/platform/web/ui/session/room/timeline/VerificationTileView.ts @@ -0,0 +1,42 @@ +/* +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 type {VerificationTile} from "../../../../../../domain/session/room/timeline/tiles/VerificationTile"; + +export class VerificationTileView extends TemplateView { + render(t: Builder, vm: VerificationTile) { + return t.div( { className: "VerificationTileContainer" }, + t.div({ className: "VerificationTileView" }, [ + t.div({className: "VerificationTileView__shield"}), + t.div({ className: "VerificationTileView__description" }, 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"), + ]), + ]) + ); + } + + /* This is called by the parent ListView, which just has 1 listener for the whole list */ + onClick(evt) { + if (evt.target.classList.contains("VerificationTileView__accept")) { + this.value.accept(); + } else if (evt.target.classList.contains("VerificationTileView__reject")) { + this.value.reject(); + } + } +}