From 3bb889ed9c1efb6c093811adb9bc1e79a885f74d Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 23 Jan 2023 14:53:08 +0100 Subject: [PATCH] WIP --- .../session/room/timeline/tiles/CallTile.js | 61 ++++++++++++++++--- src/matrix/calls/CallHandler.ts | 4 +- src/matrix/calls/callEventTypes.ts | 5 ++ src/matrix/calls/group/GroupCall.ts | 8 ++- .../web/ui/css/themes/element/timeline.css | 9 ++- .../ui/session/room/timeline/CallTileView.ts | 12 +++- 6 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/CallTile.js b/src/domain/session/room/timeline/tiles/CallTile.js index c792496e..1758e6bb 100644 --- a/src/domain/session/room/timeline/tiles/CallTile.js +++ b/src/domain/session/room/timeline/tiles/CallTile.js @@ -15,7 +15,11 @@ limitations under the License. */ import {SimpleTile} from "./SimpleTile.js"; +import {ViewModel} from "../../../../ViewModel"; import {LocalMedia} from "../../../../../matrix/calls/LocalMedia"; +import {CallType} from "../../../../../matrix/calls/callEventTypes"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../../../avatar"; + // TODO: timeline entries for state events with the same state key and type // should also update previous entries in the timeline, so we can update the name of the call, whether it is terminated, etc ... @@ -28,17 +32,28 @@ export class CallTile extends SimpleTile { this._call = calls.get(this._entry.stateKey); this._callSubscription = undefined; if (this._call) { - this._callSubscription = this._call.disposableOn("change", () => { + this._callSubscription = this.track(this._call.disposableOn("change", () => { // unsubscribe when terminated if (this._call.isTerminated) { this._callSubscription = this._callSubscription(); this._call = undefined; } this.emitChange(); - }); + })); + this.memberViewModels = this._setupMembersList(this._call); } } + _setupMembersList(call) { + return call.members.mapValues( + (member, emitChange) => new MemberAvatarViewModel(this.childOptions({ + member, + emitChange, + mediaRepository: this.getOption("room").mediaRepository + })), + ).sortValues((a, b) => a.avatarTitle < b.avatarTitle ? -1 : 1); + } + get confId() { return this._entry.stateKey; } @@ -61,16 +76,24 @@ export class CallTile extends SimpleTile { get label() { if (this._call) { - if (this._call.hasJoined) { - return `Ongoing call (${this.name}, ${this.confId})`; + if (this._type === CallType.Video) { + return `${this.displayName} started a video call`; } else { - return `${this.displayName} started a call (${this.name}, ${this.confId})`; + return `${this.displayName} started a voice call`; } } else { - return `Call finished, started by ${this.displayName} (${this.name}, ${this.confId})`; + if (this._type === CallType.Video) { + return `Video call ended`; + } else { + return `Voice call ended`; + } } } + get _type() { + return this._entry.event.content["m.type"]; + } + async join() { await this.logAndCatch("CallTile.join", async log => { if (this.canJoin) { @@ -88,10 +111,28 @@ export class CallTile extends SimpleTile { } }); } +} - dispose() { - if (this._callSubscription) { - this._callSubscription = this._callSubscription(); - } +class MemberAvatarViewModel extends ViewModel { + get _member() { + return this.getOption("member"); + } + + get avatarLetter() { + return avatarInitials(this._member.member.name); + } + + get avatarColorNumber() { + return getIdentifierColorNumber(this._member.userId); + } + + avatarUrl(size) { + const {avatarUrl} = this._member.member; + const mediaRepository = this.getOption("mediaRepository"); + return getAvatarHttpUrl(avatarUrl, size, this.platform, mediaRepository); + } + + get avatarTitle() { + return this._member.member.name; } } diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index 6be6b193..f6533f4f 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -18,7 +18,7 @@ import {ObservableMap} from "../../observable/map"; import {WebRTC, PeerConnection} from "../../platform/types/WebRTC"; import {MediaDevices, Track} from "../../platform/types/MediaDevices"; import {handlesEventType} from "./PeerCall"; -import {EventType, CallIntent} from "./callEventTypes"; +import {EventType, CallIntent, CallType} from "./callEventTypes"; import {GroupCall} from "./group/GroupCall"; import {makeId} from "../common"; import {CALL_LOG_TYPE} from "./common"; @@ -130,7 +130,7 @@ export class CallHandler implements RoomStateHandler { log.set("newSize", this._calls.size); } - createCall(roomId: string, type: "m.video" | "m.voice", name: string, intent?: CallIntent, log?: ILogItem): Promise { + createCall(roomId: string, type: CallType, name: string, intent?: CallIntent, log?: ILogItem): Promise { return this.options.logger.wrapOrRun(log, "CallHandler.createCall", async log => { if (!intent) { intent = CallIntent.Ring; diff --git a/src/matrix/calls/callEventTypes.ts b/src/matrix/calls/callEventTypes.ts index 8fd7b23d..a0eb986c 100644 --- a/src/matrix/calls/callEventTypes.ts +++ b/src/matrix/calls/callEventTypes.ts @@ -227,3 +227,8 @@ export enum CallIntent { Prompt = "m.prompt", Room = "m.room", }; + +export enum CallType { + Video = "m.video", + Voice = "m.voice", +} diff --git a/src/matrix/calls/group/GroupCall.ts b/src/matrix/calls/group/GroupCall.ts index 0b8c7db5..241e486c 100644 --- a/src/matrix/calls/group/GroupCall.ts +++ b/src/matrix/calls/group/GroupCall.ts @@ -20,7 +20,7 @@ import {LocalMedia} from "../LocalMedia"; import {MuteSettings, CALL_LOG_TYPE, CALL_MEMBER_VALIDITY_PERIOD_MS, mute} from "../common"; import {MemberChange, RoomMember} from "../../room/members/RoomMember"; import {EventEmitter} from "../../../utils/EventEmitter"; -import {EventType, CallIntent} from "../callEventTypes"; +import {EventType, CallIntent, CallType} from "../callEventTypes"; import { ErrorBoundary } from "../../../utils/ErrorBoundary"; import type {Options as MemberOptions} from "./Member"; @@ -155,6 +155,10 @@ export class GroupCall extends EventEmitter<{change: never}> { return this.callContent?.["m.intent"]; } + get type(): CallType { + return this.callContent?.["m.type"]; + } + /** * Gives access the log item for this call while joined. * Can be used for call diagnostics while in the call. @@ -318,7 +322,7 @@ export class GroupCall extends EventEmitter<{change: never}> { } /** @internal */ - create(type: "m.video" | "m.voice", log: ILogItem): Promise { + create(type: CallType, log: ILogItem): Promise { return log.wrap({l: "create call", t: CALL_LOG_TYPE}, async log => { if (this._state !== GroupCallState.Fledgling) { return; diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 7ff35eb1..81a81960 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -440,4 +440,11 @@ only loads when the top comes into view*/ background-color: var(--background-color-primary); border-radius: 8px; text-align: center; - } +} + +.CallTileView_members > * { + margin-left: -16px; +} +.CallTileView_members { + display: flex; +} diff --git a/src/platform/web/ui/session/room/timeline/CallTileView.ts b/src/platform/web/ui/session/room/timeline/CallTileView.ts index 5bede510..c99c2136 100644 --- a/src/platform/web/ui/session/room/timeline/CallTileView.ts +++ b/src/platform/web/ui/session/room/timeline/CallTileView.ts @@ -17,7 +17,16 @@ limitations under the License. import {Builder, TemplateView} from "../../../general/TemplateView"; import type {CallTile} from "../../../../../../domain/session/room/timeline/tiles/CallTile"; import {ErrorView} from "../../../general/ErrorView"; - +import {ListView} from "../../../general/ListView"; +import {AvatarView} from "../../../AvatarView"; +/* +.CallTileView_members > * { + margin-left: -16px; +} +.CallTileView_members { + display: flex; +} +*/ export class CallTileView extends TemplateView { render(t: Builder, vm: CallTile) { return t.li( @@ -29,6 +38,7 @@ export class CallTileView extends TemplateView { }), t.div([ vm => vm.label, + t.view(new ListView({className: "CallTileView_members", list: vm.memberViewModels}, vm => new AvatarView(vm, 24))), t.button({className: "CallTileView_join", hidden: vm => !vm.canJoin}, "Join"), t.button({className: "CallTileView_leave", hidden: vm => !vm.canLeave}, "Leave") ])