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 01/10] 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") ]) From af5cc0f62b709724e661de8220f5c0b89b34886c Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:35:28 +0100 Subject: [PATCH 02/10] sort by userId, sorting order needs to be stable --- src/domain/session/room/timeline/tiles/CallTile.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/CallTile.js b/src/domain/session/room/timeline/tiles/CallTile.js index 1758e6bb..b762cef4 100644 --- a/src/domain/session/room/timeline/tiles/CallTile.js +++ b/src/domain/session/room/timeline/tiles/CallTile.js @@ -51,7 +51,7 @@ export class CallTile extends SimpleTile { emitChange, mediaRepository: this.getOption("room").mediaRepository })), - ).sortValues((a, b) => a.avatarTitle < b.avatarTitle ? -1 : 1); + ).sortValues((a, b) => a.userId.localeCompare(b.userId)); } get confId() { @@ -118,6 +118,10 @@ class MemberAvatarViewModel extends ViewModel { return this.getOption("member"); } + get userId() { + return this._member.userId; + } + get avatarLetter() { return avatarInitials(this._member.member.name); } From 1df8d31ab5f4b812544b6ff18c7a4a389aae9854 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 23 Jan 2023 17:06:33 +0100 Subject: [PATCH 03/10] show call duration in tile --- .../session/room/timeline/tiles/CallTile.js | 30 +++++++++++++++---- src/matrix/calls/CallHandler.ts | 8 ++--- src/matrix/calls/group/GroupCall.ts | 15 ++++++++-- src/platform/types/types.ts | 1 + src/platform/web/dom/TimeFormatter.ts | 24 +++++++++++++++ .../ui/session/room/timeline/CallTileView.ts | 10 ++----- 6 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/CallTile.js b/src/domain/session/room/timeline/tiles/CallTile.js index b762cef4..71297646 100644 --- a/src/domain/session/room/timeline/tiles/CallTile.js +++ b/src/domain/session/room/timeline/tiles/CallTile.js @@ -33,17 +33,27 @@ export class CallTile extends SimpleTile { this._callSubscription = undefined; if (this._call) { 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._onCallUpdate(); })); + this._onCallUpdate(); this.memberViewModels = this._setupMembersList(this._call); } } + _onCallUpdate() { + // unsubscribe when terminated + if (this._call.isTerminated) { + this._durationInterval = this.disposeTracked(this._durationInterval); + this._callSubscription = this.disposeTracked(this._callSubscription); + this._call = undefined; + } else if (!this._durationInterval) { + this._durationInterval = this.track(this.platform.clock.createInterval(() => { + this.emitChange("duration"); + }, 1000)); + } + this.emitChange(); + } + _setupMembersList(call) { return call.members.mapValues( (member, emitChange) => new MemberAvatarViewModel(this.childOptions({ @@ -57,6 +67,14 @@ export class CallTile extends SimpleTile { get confId() { return this._entry.stateKey; } + + get duration() { + if (this._call && this._call.duration) { + return this.timeFormatter.formatDuration(this._call.duration); + } else { + return ""; + } + } get shape() { return "call"; diff --git a/src/matrix/calls/CallHandler.ts b/src/matrix/calls/CallHandler.ts index f6533f4f..c3f5d917 100644 --- a/src/matrix/calls/CallHandler.ts +++ b/src/matrix/calls/CallHandler.ts @@ -104,7 +104,7 @@ export class CallHandler implements RoomStateHandler { } const event = await txn.roomState.get(callEntry.roomId, EventType.GroupCall, callEntry.callId); if (event) { - const call = new GroupCall(event.event.state_key, false, event.event.content, event.roomId, this.groupCallOptions); + const call = new GroupCall(event.event.state_key, false, callEntry.timestamp, event.event.content, event.roomId, this.groupCallOptions); this._calls.set(call.id, call); } })); @@ -135,7 +135,7 @@ export class CallHandler implements RoomStateHandler { if (!intent) { intent = CallIntent.Ring; } - const call = new GroupCall(makeId("conf-"), true, { + const call = new GroupCall(makeId("conf-"), true, undefined, { "m.name": name, "m.intent": intent }, roomId, this.groupCallOptions); @@ -210,14 +210,14 @@ export class CallHandler implements RoomStateHandler { const callId = event.state_key; let call = this._calls.get(callId); if (call) { - call.updateCallEvent(event.content, log); + call.updateCallEvent(event, log); if (call.isTerminated) { call.disconnect(log); this._calls.remove(call.id); txn.calls.remove(call.intent, roomId, call.id); } } else { - call = new GroupCall(event.state_key, false, event.content, roomId, this.groupCallOptions); + call = new GroupCall(event.state_key, false, event.origin_server_ts, event.content, roomId, this.groupCallOptions); this._calls.set(call.id, call); txn.calls.add({ intent: call.intent, diff --git a/src/matrix/calls/group/GroupCall.ts b/src/matrix/calls/group/GroupCall.ts index 241e486c..5748601f 100644 --- a/src/matrix/calls/group/GroupCall.ts +++ b/src/matrix/calls/group/GroupCall.ts @@ -105,6 +105,7 @@ export class GroupCall extends EventEmitter<{change: never}> { constructor( public readonly id: string, newCall: boolean, + private startTime: number | undefined, private callContent: Record, public readonly roomId: string, private readonly options: Options, @@ -143,6 +144,12 @@ export class GroupCall extends EventEmitter<{change: never}> { return this.callContent?.["m.terminated"] === true; } + get duration(): number | undefined { + if (typeof this.startTime === "number") { + return (this.options.clock.now() - this.startTime); + } + } + get isRinging(): boolean { return this._state === GroupCallState.Created && this.intent === "m.ring" && !this.isMember(this.options.ownUserId); } @@ -340,10 +347,14 @@ export class GroupCall extends EventEmitter<{change: never}> { } /** @internal */ - updateCallEvent(callContent: Record, syncLog: ILogItem) { + updateCallEvent(event: StateEvent, syncLog: ILogItem) { this.errorBoundary.try(() => { syncLog.wrap({l: "update call", t: CALL_LOG_TYPE, id: this.id}, log => { - this.callContent = callContent; + + if (typeof this.startTime !== "number") { + this.startTime = event.origin_server_ts; + } + this.callContent = event.content; if (this._state === GroupCallState.Creating) { this._state = GroupCallState.Created; } diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 77953c4a..0e2f536e 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -55,4 +55,5 @@ export interface ITimeFormatter { formatTime(date: Date): string; formatRelativeDate(date: Date): string; formatMachineReadableDate(date: Date): string; + formatDuration(milliseconds: number): string; } diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts index 2a98a716..cebc6ec4 100644 --- a/src/platform/web/dom/TimeFormatter.ts +++ b/src/platform/web/dom/TimeFormatter.ts @@ -22,6 +22,9 @@ enum TimeScope { Day = 24 * 60 * 60 * 1000, } +const MINUTES_IN_MS = 60 * 1000; +const HOURS_IN_MS = MINUTES_IN_MS * 60; + export class TimeFormatter implements ITimeFormatter { private todayMidnight: Date; @@ -75,6 +78,27 @@ export class TimeFormatter implements ITimeFormatter { return this.otherYearFormatter.format(date); } } + + formatDuration(milliseconds: number): string { + let hours = 0; + let minutes = 0; + if (milliseconds > HOURS_IN_MS) { + hours = Math.floor(milliseconds / HOURS_IN_MS); + milliseconds -= hours * HOURS_IN_MS; + } + if (milliseconds > MINUTES_IN_MS) { + minutes = Math.floor(milliseconds / MINUTES_IN_MS); + milliseconds -= minutes * MINUTES_IN_MS; + } + const seconds = Math.floor(milliseconds / 1000); + if (hours) { + return `${hours}h ${minutes}m ${seconds}s`; + } + if (minutes) { + return `${minutes}m ${seconds}s`; + } + return `${seconds}s`; + } } function capitalizeFirstLetter(str: string) { diff --git a/src/platform/web/ui/session/room/timeline/CallTileView.ts b/src/platform/web/ui/session/room/timeline/CallTileView.ts index c99c2136..a5c6ae09 100644 --- a/src/platform/web/ui/session/room/timeline/CallTileView.ts +++ b/src/platform/web/ui/session/room/timeline/CallTileView.ts @@ -19,14 +19,7 @@ import type {CallTile} from "../../../../../../domain/session/room/timeline/tile 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( @@ -38,6 +31,7 @@ export class CallTileView extends TemplateView { }), t.div([ vm => vm.label, + vm => vm.duration, 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") From 5035d2357349032aa28b8a3c866f8a1de861d03f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Mon, 23 Jan 2023 17:42:33 +0100 Subject: [PATCH 04/10] add all elements to call tile that need to be shown, style buttons --- .../session/room/timeline/tiles/CallTile.js | 28 +++++++++++++------ .../web/ui/css/themes/element/timeline.css | 14 ++++++++++ .../ui/session/room/timeline/CallTileView.ts | 22 ++++++++++----- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/CallTile.js b/src/domain/session/room/timeline/tiles/CallTile.js index 71297646..8887b834 100644 --- a/src/domain/session/room/timeline/tiles/CallTile.js +++ b/src/domain/session/room/timeline/tiles/CallTile.js @@ -64,6 +64,14 @@ export class CallTile extends SimpleTile { ).sortValues((a, b) => a.userId.localeCompare(b.userId)); } + get memberCount() { + // TODO: emit updates for this property + if (this._call) { + return this._call.members.size; + } + return 0; + } + get confId() { return this._entry.stateKey; } @@ -80,10 +88,6 @@ export class CallTile extends SimpleTile { return "call"; } - get name() { - return this._entry.content["m.name"]; - } - get canJoin() { return this._call && !this._call.hasJoined; } @@ -92,15 +96,15 @@ export class CallTile extends SimpleTile { return this._call && this._call.hasJoined; } - get label() { + get title() { if (this._call) { - if (this._type === CallType.Video) { + if (this.type === CallType.Video) { return `${this.displayName} started a video call`; } else { return `${this.displayName} started a voice call`; } } else { - if (this._type === CallType.Video) { + if (this.type === CallType.Video) { return `Video call ended`; } else { return `Voice call ended`; @@ -108,7 +112,15 @@ export class CallTile extends SimpleTile { } } - get _type() { + get typeLabel() { + if (this.type === CallType.Video) { + return `Video call`; + } else { + return `Voice call`; + } + } + + get type() { return this._entry.event.content["m.type"]; } diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 81a81960..5dc86276 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -442,9 +442,23 @@ only loads when the top comes into view*/ text-align: center; } +.CallTileView > div > div { + display: flex; + flex-direction: column; + gap: 4px; +} + .CallTileView_members > * { margin-left: -16px; } .CallTileView_members { display: flex; } + +.CallTileView_memberCount::before { + content: url('./icons/room-members.svg?primary=text-color'); + background-repeat: no-repeat; + background-size: 24px; + width: 24px; + height: 24px; +} diff --git a/src/platform/web/ui/session/room/timeline/CallTileView.ts b/src/platform/web/ui/session/room/timeline/CallTileView.ts index a5c6ae09..a9a872b3 100644 --- a/src/platform/web/ui/session/room/timeline/CallTileView.ts +++ b/src/platform/web/ui/session/room/timeline/CallTileView.ts @@ -30,11 +30,19 @@ export class CallTileView extends TemplateView { return t.div({className: "CallTileView_error"}, t.view(new ErrorView(vm.errorViewModel, {inline: true}))); }), t.div([ - vm => vm.label, - vm => vm.duration, - 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") + t.div({className: "CallTileView_title"}, vm => vm.title), + t.div({className: "CallTileView_subtitle"}, [ + vm.typeLabel, " • ", + t.span({className: "CallTileView_memberCount"}, vm => vm.memberCount) + ]), + t.view(new ListView({className: "CallTileView_members", tagName: "div", list: vm.memberViewModels}, vm => { + return new AvatarView(vm, 24); + })), + t.div(vm => vm.duration), + t.div([ + t.button({className: "CallTileView_join button-action primary", hidden: vm => !vm.canJoin}, "Join"), + t.button({className: "CallTileView_leave button-action primary destructive", hidden: vm => !vm.canLeave}, "Leave") + ]) ]) ]) ); @@ -42,9 +50,9 @@ export class CallTileView extends TemplateView { /* This is called by the parent ListView, which just has 1 listener for the whole list */ onClick(evt) { - if (evt.target.className === "CallTileView_join") { + if (evt.target.classList.contains("CallTileView_join")) { this.value.join(); - } else if (evt.target.className === "CallTileView_leave") { + } else if (evt.target.classList.contains("CallTileView_leave")) { this.value.leave(); } } From c2fab59f582943e59f89bce7c8c1afce580c1375 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 24 Jan 2023 23:15:32 +0100 Subject: [PATCH 05/10] ensure call isn't cleared by onCallUpdate when setting up member list --- src/domain/session/room/timeline/tiles/CallTile.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/CallTile.js b/src/domain/session/room/timeline/tiles/CallTile.js index 8887b834..4583de49 100644 --- a/src/domain/session/room/timeline/tiles/CallTile.js +++ b/src/domain/session/room/timeline/tiles/CallTile.js @@ -29,14 +29,15 @@ export class CallTile extends SimpleTile { constructor(entry, options) { super(entry, options); const calls = this.getOption("session").callHandler.calls; - this._call = calls.get(this._entry.stateKey); this._callSubscription = undefined; - if (this._call) { + const call = calls.get(this._entry.stateKey); + if (call && !call.isTerminated) { + this._call = call; + this.memberViewModels = this._setupMembersList(this._call); this._callSubscription = this.track(this._call.disposableOn("change", () => { this._onCallUpdate(); })); this._onCallUpdate(); - this.memberViewModels = this._setupMembersList(this._call); } } From 98416f8c35bf3e8da6b25628bd06c334de92d02f Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 24 Jan 2023 23:17:04 +0100 Subject: [PATCH 06/10] also calculate days in formatDuration --- src/platform/web/dom/TimeFormatter.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts index cebc6ec4..916b18bb 100644 --- a/src/platform/web/dom/TimeFormatter.ts +++ b/src/platform/web/dom/TimeFormatter.ts @@ -24,6 +24,7 @@ enum TimeScope { const MINUTES_IN_MS = 60 * 1000; const HOURS_IN_MS = MINUTES_IN_MS * 60; +const DAYS_IN_MS = HOURS_IN_MS * 24; export class TimeFormatter implements ITimeFormatter { @@ -80,8 +81,13 @@ export class TimeFormatter implements ITimeFormatter { } formatDuration(milliseconds: number): string { + let days = 0; let hours = 0; let minutes = 0; + if (milliseconds > DAYS_IN_MS) { + days = Math.floor(milliseconds / DAYS_IN_MS); + milliseconds -= days * DAYS_IN_MS; + } if (milliseconds > HOURS_IN_MS) { hours = Math.floor(milliseconds / HOURS_IN_MS); milliseconds -= hours * HOURS_IN_MS; @@ -91,13 +97,18 @@ export class TimeFormatter implements ITimeFormatter { milliseconds -= minutes * MINUTES_IN_MS; } const seconds = Math.floor(milliseconds / 1000); - if (hours) { - return `${hours}h ${minutes}m ${seconds}s`; + let result = ""; + if (days) { + result = `${days}d `; } - if (minutes) { - return `${minutes}m ${seconds}s`; + if (hours || days) { + result += `${hours}h `; } - return `${seconds}s`; + if (minutes || hours || days) { + result += `${minutes}m `; + } + result += `${seconds}s`; + return result; } } From 47d9773fc89c0edb948c371f76df4435ae895334 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Tue, 24 Jan 2023 23:17:20 +0100 Subject: [PATCH 07/10] more style changes --- .../web/ui/css/themes/element/timeline.css | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 5dc86276..16c758a3 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -449,16 +449,26 @@ only loads when the top comes into view*/ } .CallTileView_members > * { - margin-left: -16px; + margin-right: -16px; } + .CallTileView_members { display: flex; } +.CallTileView_title { + font-weight: bold; +} + +.CallTileView_subtitle { + font-size: 12px; +} + .CallTileView_memberCount::before { content: url('./icons/room-members.svg?primary=text-color'); - background-repeat: no-repeat; - background-size: 24px; - width: 24px; - height: 24px; + width: 16px; + height: 16px; + display: inline-flex; + vertical-align: bottom; + margin-right: 4px; } From a278086c3787366cf8738cc015c03629d08d93a4 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 26 Jan 2023 09:58:24 +0100 Subject: [PATCH 08/10] fix error at time unit boundary --- src/platform/web/dom/TimeFormatter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts index 916b18bb..090f8076 100644 --- a/src/platform/web/dom/TimeFormatter.ts +++ b/src/platform/web/dom/TimeFormatter.ts @@ -84,15 +84,15 @@ export class TimeFormatter implements ITimeFormatter { let days = 0; let hours = 0; let minutes = 0; - if (milliseconds > DAYS_IN_MS) { + if (milliseconds >= DAYS_IN_MS) { days = Math.floor(milliseconds / DAYS_IN_MS); milliseconds -= days * DAYS_IN_MS; } - if (milliseconds > HOURS_IN_MS) { + if (milliseconds >= HOURS_IN_MS) { hours = Math.floor(milliseconds / HOURS_IN_MS); milliseconds -= hours * HOURS_IN_MS; } - if (milliseconds > MINUTES_IN_MS) { + if (milliseconds >= MINUTES_IN_MS) { minutes = Math.floor(milliseconds / MINUTES_IN_MS); milliseconds -= minutes * MINUTES_IN_MS; } From 0f91f2065cb069dffd6e985dd53adbc88923ef15 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 26 Jan 2023 10:25:22 +0100 Subject: [PATCH 09/10] extract to src/utils as it doesn't assume the DOM --- src/platform/web/dom/TimeFormatter.ts | 39 ++------------------------- src/utils/timeFormatting.ts | 36 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 37 deletions(-) create mode 100644 src/utils/timeFormatting.ts diff --git a/src/platform/web/dom/TimeFormatter.ts b/src/platform/web/dom/TimeFormatter.ts index 090f8076..c25e902b 100644 --- a/src/platform/web/dom/TimeFormatter.ts +++ b/src/platform/web/dom/TimeFormatter.ts @@ -16,15 +16,7 @@ limitations under the License. import type { ITimeFormatter } from "../../types/types"; import {Clock} from "./Clock"; - -enum TimeScope { - Minute = 60 * 1000, - Day = 24 * 60 * 60 * 1000, -} - -const MINUTES_IN_MS = 60 * 1000; -const HOURS_IN_MS = MINUTES_IN_MS * 60; -const DAYS_IN_MS = HOURS_IN_MS * 24; +import {formatDuration, TimeScope} from "../../../utils/timeFormatting"; export class TimeFormatter implements ITimeFormatter { @@ -81,34 +73,7 @@ export class TimeFormatter implements ITimeFormatter { } formatDuration(milliseconds: number): string { - let days = 0; - let hours = 0; - let minutes = 0; - if (milliseconds >= DAYS_IN_MS) { - days = Math.floor(milliseconds / DAYS_IN_MS); - milliseconds -= days * DAYS_IN_MS; - } - if (milliseconds >= HOURS_IN_MS) { - hours = Math.floor(milliseconds / HOURS_IN_MS); - milliseconds -= hours * HOURS_IN_MS; - } - if (milliseconds >= MINUTES_IN_MS) { - minutes = Math.floor(milliseconds / MINUTES_IN_MS); - milliseconds -= minutes * MINUTES_IN_MS; - } - const seconds = Math.floor(milliseconds / 1000); - let result = ""; - if (days) { - result = `${days}d `; - } - if (hours || days) { - result += `${hours}h `; - } - if (minutes || hours || days) { - result += `${minutes}m `; - } - result += `${seconds}s`; - return result; + return formatDuration(milliseconds); } } diff --git a/src/utils/timeFormatting.ts b/src/utils/timeFormatting.ts new file mode 100644 index 00000000..8d052e95 --- /dev/null +++ b/src/utils/timeFormatting.ts @@ -0,0 +1,36 @@ +export enum TimeScope { + Minute = 60 * 1000, + Hours = 60 * TimeScope.Minute, + Day = 24 * TimeScope.Hours, +} + +export function formatDuration(milliseconds: number): string { + let days = 0; + let hours = 0; + let minutes = 0; + if (milliseconds >= TimeScope.Day) { + days = Math.floor(milliseconds / TimeScope.Day); + milliseconds -= days * TimeScope.Day; + } + if (milliseconds >= TimeScope.Hours) { + hours = Math.floor(milliseconds / TimeScope.Hours); + milliseconds -= hours * TimeScope.Hours; + } + if (milliseconds >= TimeScope.Minute) { + minutes = Math.floor(milliseconds / TimeScope.Minute); + milliseconds -= minutes * TimeScope.Minute; + } + const seconds = Math.floor(milliseconds / 1000); + let result = ""; + if (days) { + result = `${days}d `; + } + if (hours || days) { + result += `${hours}h `; + } + if (minutes || hours || days) { + result += `${minutes}m `; + } + result += `${seconds}s`; + return result; +} From cd76619953fde153c257b0b6486129eeeafeb859 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Thu, 26 Jan 2023 10:30:29 +0100 Subject: [PATCH 10/10] add header --- src/utils/timeFormatting.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/timeFormatting.ts b/src/utils/timeFormatting.ts index 8d052e95..2f2ae8c0 100644 --- a/src/utils/timeFormatting.ts +++ b/src/utils/timeFormatting.ts @@ -1,3 +1,19 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + export enum TimeScope { Minute = 60 * 1000, Hours = 60 * TimeScope.Minute,