mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
Merge pull request #1003 from vector-im/bwindels/calltile-ui
Improve CallTile UI
This commit is contained in:
commit
38d5a4412b
@ -15,7 +15,11 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {SimpleTile} from "./SimpleTile.js";
|
import {SimpleTile} from "./SimpleTile.js";
|
||||||
|
import {ViewModel} from "../../../../ViewModel";
|
||||||
import {LocalMedia} from "../../../../../matrix/calls/LocalMedia";
|
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
|
// 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 ...
|
// should also update previous entries in the timeline, so we can update the name of the call, whether it is terminated, etc ...
|
||||||
|
|
||||||
@ -25,32 +29,66 @@ export class CallTile extends SimpleTile {
|
|||||||
constructor(entry, options) {
|
constructor(entry, options) {
|
||||||
super(entry, options);
|
super(entry, options);
|
||||||
const calls = this.getOption("session").callHandler.calls;
|
const calls = this.getOption("session").callHandler.calls;
|
||||||
this._call = calls.get(this._entry.stateKey);
|
|
||||||
this._callSubscription = undefined;
|
this._callSubscription = undefined;
|
||||||
if (this._call) {
|
const call = calls.get(this._entry.stateKey);
|
||||||
this._callSubscription = this._call.disposableOn("change", () => {
|
if (call && !call.isTerminated) {
|
||||||
// unsubscribe when terminated
|
this._call = call;
|
||||||
if (this._call.isTerminated) {
|
this.memberViewModels = this._setupMembersList(this._call);
|
||||||
this._callSubscription = this._callSubscription();
|
this._callSubscription = this.track(this._call.disposableOn("change", () => {
|
||||||
this._call = undefined;
|
this._onCallUpdate();
|
||||||
}
|
}));
|
||||||
this.emitChange();
|
this._onCallUpdate();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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({
|
||||||
|
member,
|
||||||
|
emitChange,
|
||||||
|
mediaRepository: this.getOption("room").mediaRepository
|
||||||
|
})),
|
||||||
|
).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() {
|
get confId() {
|
||||||
return this._entry.stateKey;
|
return this._entry.stateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get duration() {
|
||||||
|
if (this._call && this._call.duration) {
|
||||||
|
return this.timeFormatter.formatDuration(this._call.duration);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get shape() {
|
get shape() {
|
||||||
return "call";
|
return "call";
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this._entry.content["m.name"];
|
|
||||||
}
|
|
||||||
|
|
||||||
get canJoin() {
|
get canJoin() {
|
||||||
return this._call && !this._call.hasJoined;
|
return this._call && !this._call.hasJoined;
|
||||||
}
|
}
|
||||||
@ -59,18 +97,34 @@ export class CallTile extends SimpleTile {
|
|||||||
return this._call && this._call.hasJoined;
|
return this._call && this._call.hasJoined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get label() {
|
get title() {
|
||||||
if (this._call) {
|
if (this._call) {
|
||||||
if (this._call.hasJoined) {
|
if (this.type === CallType.Video) {
|
||||||
return `Ongoing call (${this.name}, ${this.confId})`;
|
return `${this.displayName} started a video call`;
|
||||||
} else {
|
} else {
|
||||||
return `${this.displayName} started a call (${this.name}, ${this.confId})`;
|
return `${this.displayName} started a voice call`;
|
||||||
}
|
}
|
||||||
} else {
|
} 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 typeLabel() {
|
||||||
|
if (this.type === CallType.Video) {
|
||||||
|
return `Video call`;
|
||||||
|
} else {
|
||||||
|
return `Voice call`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return this._entry.event.content["m.type"];
|
||||||
|
}
|
||||||
|
|
||||||
async join() {
|
async join() {
|
||||||
await this.logAndCatch("CallTile.join", async log => {
|
await this.logAndCatch("CallTile.join", async log => {
|
||||||
if (this.canJoin) {
|
if (this.canJoin) {
|
||||||
@ -88,10 +142,32 @@ export class CallTile extends SimpleTile {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
class MemberAvatarViewModel extends ViewModel {
|
||||||
if (this._callSubscription) {
|
get _member() {
|
||||||
this._callSubscription = this._callSubscription();
|
return this.getOption("member");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userId() {
|
||||||
|
return this._member.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import {ObservableMap} from "../../observable/map";
|
|||||||
import {WebRTC, PeerConnection} from "../../platform/types/WebRTC";
|
import {WebRTC, PeerConnection} from "../../platform/types/WebRTC";
|
||||||
import {MediaDevices, Track} from "../../platform/types/MediaDevices";
|
import {MediaDevices, Track} from "../../platform/types/MediaDevices";
|
||||||
import {handlesEventType} from "./PeerCall";
|
import {handlesEventType} from "./PeerCall";
|
||||||
import {EventType, CallIntent} from "./callEventTypes";
|
import {EventType, CallIntent, CallType} from "./callEventTypes";
|
||||||
import {GroupCall} from "./group/GroupCall";
|
import {GroupCall} from "./group/GroupCall";
|
||||||
import {makeId} from "../common";
|
import {makeId} from "../common";
|
||||||
import {CALL_LOG_TYPE} from "./common";
|
import {CALL_LOG_TYPE} from "./common";
|
||||||
@ -104,7 +104,15 @@ export class CallHandler implements RoomStateHandler {
|
|||||||
}
|
}
|
||||||
const event = await txn.roomState.get(callEntry.roomId, EventType.GroupCall, callEntry.callId);
|
const event = await txn.roomState.get(callEntry.roomId, EventType.GroupCall, callEntry.callId);
|
||||||
if (event) {
|
if (event) {
|
||||||
const call = new GroupCall(event.event.state_key, true, false, event.event.content, event.roomId, this.groupCallOptions);
|
const call = new GroupCall(
|
||||||
|
event.event.state_key, // id
|
||||||
|
true, // isLoadedFromStorage
|
||||||
|
false, // newCall
|
||||||
|
callEntry.timestamp, // startTime
|
||||||
|
event.event.content, // callContent
|
||||||
|
event.roomId, // roomId
|
||||||
|
this.groupCallOptions // options
|
||||||
|
);
|
||||||
this._calls.set(call.id, call);
|
this._calls.set(call.id, call);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -130,15 +138,20 @@ export class CallHandler implements RoomStateHandler {
|
|||||||
log.set("newSize", this._calls.size);
|
log.set("newSize", this._calls.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
createCall(roomId: string, type: "m.video" | "m.voice", name: string, intent?: CallIntent, log?: ILogItem): Promise<GroupCall> {
|
createCall(roomId: string, type: CallType, name: string, intent?: CallIntent, log?: ILogItem): Promise<GroupCall> {
|
||||||
return this.options.logger.wrapOrRun(log, "CallHandler.createCall", async log => {
|
return this.options.logger.wrapOrRun(log, "CallHandler.createCall", async log => {
|
||||||
if (!intent) {
|
if (!intent) {
|
||||||
intent = CallIntent.Ring;
|
intent = CallIntent.Ring;
|
||||||
}
|
}
|
||||||
const call = new GroupCall(makeId("conf-"), false, true, {
|
const call = new GroupCall(
|
||||||
"m.name": name,
|
makeId("conf-"), // id
|
||||||
"m.intent": intent
|
false, // isLoadedFromStorage
|
||||||
}, roomId, this.groupCallOptions);
|
true, // newCall
|
||||||
|
undefined, // startTime
|
||||||
|
{"m.name": name, "m.intent": intent}, // callContent
|
||||||
|
roomId, // roomId
|
||||||
|
this.groupCallOptions // options
|
||||||
|
);
|
||||||
this._calls.set(call.id, call);
|
this._calls.set(call.id, call);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -210,14 +223,22 @@ export class CallHandler implements RoomStateHandler {
|
|||||||
const callId = event.state_key;
|
const callId = event.state_key;
|
||||||
let call = this._calls.get(callId);
|
let call = this._calls.get(callId);
|
||||||
if (call) {
|
if (call) {
|
||||||
call.updateCallEvent(event.content, log);
|
call.updateCallEvent(event, log);
|
||||||
if (call.isTerminated) {
|
if (call.isTerminated) {
|
||||||
call.disconnect(log);
|
call.disconnect(log);
|
||||||
this._calls.remove(call.id);
|
this._calls.remove(call.id);
|
||||||
txn.calls.remove(call.intent, roomId, call.id);
|
txn.calls.remove(call.intent, roomId, call.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
call = new GroupCall(event.state_key, false, false, event.content, roomId, this.groupCallOptions);
|
call = new GroupCall(
|
||||||
|
event.state_key, // id
|
||||||
|
false, // isLoadedFromStorage
|
||||||
|
false, // newCall
|
||||||
|
event.origin_server_ts, // startTime
|
||||||
|
event.content, // callContent
|
||||||
|
roomId, // roomId
|
||||||
|
this.groupCallOptions // options
|
||||||
|
);
|
||||||
this._calls.set(call.id, call);
|
this._calls.set(call.id, call);
|
||||||
txn.calls.add({
|
txn.calls.add({
|
||||||
intent: call.intent,
|
intent: call.intent,
|
||||||
|
@ -227,3 +227,8 @@ export enum CallIntent {
|
|||||||
Prompt = "m.prompt",
|
Prompt = "m.prompt",
|
||||||
Room = "m.room",
|
Room = "m.room",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum CallType {
|
||||||
|
Video = "m.video",
|
||||||
|
Voice = "m.voice",
|
||||||
|
}
|
||||||
|
@ -20,7 +20,7 @@ import {LocalMedia} from "../LocalMedia";
|
|||||||
import {MuteSettings, CALL_LOG_TYPE, CALL_MEMBER_VALIDITY_PERIOD_MS, mute} from "../common";
|
import {MuteSettings, CALL_LOG_TYPE, CALL_MEMBER_VALIDITY_PERIOD_MS, mute} from "../common";
|
||||||
import {MemberChange, RoomMember} from "../../room/members/RoomMember";
|
import {MemberChange, RoomMember} from "../../room/members/RoomMember";
|
||||||
import {EventEmitter} from "../../../utils/EventEmitter";
|
import {EventEmitter} from "../../../utils/EventEmitter";
|
||||||
import {EventType, CallIntent} from "../callEventTypes";
|
import {EventType, CallIntent, CallType} from "../callEventTypes";
|
||||||
import { ErrorBoundary } from "../../../utils/ErrorBoundary";
|
import { ErrorBoundary } from "../../../utils/ErrorBoundary";
|
||||||
|
|
||||||
import type {Options as MemberOptions} from "./Member";
|
import type {Options as MemberOptions} from "./Member";
|
||||||
@ -106,6 +106,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
|||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
public readonly isLoadedFromStorage: boolean,
|
public readonly isLoadedFromStorage: boolean,
|
||||||
newCall: boolean,
|
newCall: boolean,
|
||||||
|
private startTime: number | undefined,
|
||||||
private callContent: Record<string, any>,
|
private callContent: Record<string, any>,
|
||||||
public readonly roomId: string,
|
public readonly roomId: string,
|
||||||
private readonly options: Options,
|
private readonly options: Options,
|
||||||
@ -144,6 +145,12 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
|||||||
return !!this.callContent?.["m.terminated"];
|
return !!this.callContent?.["m.terminated"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get duration(): number | undefined {
|
||||||
|
if (typeof this.startTime === "number") {
|
||||||
|
return (this.options.clock.now() - this.startTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get isRinging(): boolean {
|
get isRinging(): boolean {
|
||||||
return this._state === GroupCallState.Created && this.intent === "m.ring" && !this.isMember(this.options.ownUserId);
|
return this._state === GroupCallState.Created && this.intent === "m.ring" && !this.isMember(this.options.ownUserId);
|
||||||
}
|
}
|
||||||
@ -156,6 +163,10 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
|||||||
return this.callContent?.["m.intent"];
|
return this.callContent?.["m.intent"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get type(): CallType {
|
||||||
|
return this.callContent?.["m.type"];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives access the log item for this call while joined.
|
* Gives access the log item for this call while joined.
|
||||||
* Can be used for call diagnostics while in the call.
|
* Can be used for call diagnostics while in the call.
|
||||||
@ -319,7 +330,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
create(type: "m.video" | "m.voice", log: ILogItem): Promise<void> {
|
create(type: CallType, log: ILogItem): Promise<void> {
|
||||||
return log.wrap({l: "create call", t: CALL_LOG_TYPE}, async log => {
|
return log.wrap({l: "create call", t: CALL_LOG_TYPE}, async log => {
|
||||||
if (this._state !== GroupCallState.Fledgling) {
|
if (this._state !== GroupCallState.Fledgling) {
|
||||||
return;
|
return;
|
||||||
@ -337,10 +348,14 @@ export class GroupCall extends EventEmitter<{change: never}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
updateCallEvent(callContent: Record<string, any>, syncLog: ILogItem) {
|
updateCallEvent(event: StateEvent, syncLog: ILogItem) {
|
||||||
this.errorBoundary.try(() => {
|
this.errorBoundary.try(() => {
|
||||||
syncLog.wrap({l: "update call", t: CALL_LOG_TYPE, id: this.id}, log => {
|
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) {
|
if (this._state === GroupCallState.Creating) {
|
||||||
this._state = GroupCallState.Created;
|
this._state = GroupCallState.Created;
|
||||||
}
|
}
|
||||||
|
@ -55,4 +55,5 @@ export interface ITimeFormatter {
|
|||||||
formatTime(date: Date): string;
|
formatTime(date: Date): string;
|
||||||
formatRelativeDate(date: Date): string;
|
formatRelativeDate(date: Date): string;
|
||||||
formatMachineReadableDate(date: Date): string;
|
formatMachineReadableDate(date: Date): string;
|
||||||
|
formatDuration(milliseconds: number): string;
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import type { ITimeFormatter } from "../../types/types";
|
import type { ITimeFormatter } from "../../types/types";
|
||||||
import {Clock} from "./Clock";
|
import {Clock} from "./Clock";
|
||||||
|
import {formatDuration, TimeScope} from "../../../utils/timeFormatting";
|
||||||
enum TimeScope {
|
|
||||||
Minute = 60 * 1000,
|
|
||||||
Day = 24 * 60 * 60 * 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TimeFormatter implements ITimeFormatter {
|
export class TimeFormatter implements ITimeFormatter {
|
||||||
|
|
||||||
@ -75,6 +71,10 @@ export class TimeFormatter implements ITimeFormatter {
|
|||||||
return this.otherYearFormatter.format(date);
|
return this.otherYearFormatter.format(date);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatDuration(milliseconds: number): string {
|
||||||
|
return formatDuration(milliseconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function capitalizeFirstLetter(str: string) {
|
function capitalizeFirstLetter(str: string) {
|
||||||
|
@ -440,4 +440,35 @@ only loads when the top comes into view*/
|
|||||||
background-color: var(--background-color-primary);
|
background-color: var(--background-color-primary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CallTileView > div > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallTileView_members > * {
|
||||||
|
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');
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
import {Builder, TemplateView} from "../../../general/TemplateView";
|
import {Builder, TemplateView} from "../../../general/TemplateView";
|
||||||
import type {CallTile} from "../../../../../../domain/session/room/timeline/tiles/CallTile";
|
import type {CallTile} from "../../../../../../domain/session/room/timeline/tiles/CallTile";
|
||||||
import {ErrorView} from "../../../general/ErrorView";
|
import {ErrorView} from "../../../general/ErrorView";
|
||||||
|
import {ListView} from "../../../general/ListView";
|
||||||
|
import {AvatarView} from "../../../AvatarView";
|
||||||
|
|
||||||
export class CallTileView extends TemplateView<CallTile> {
|
export class CallTileView extends TemplateView<CallTile> {
|
||||||
render(t: Builder<CallTile>, vm: CallTile) {
|
render(t: Builder<CallTile>, vm: CallTile) {
|
||||||
@ -28,9 +30,19 @@ export class CallTileView extends TemplateView<CallTile> {
|
|||||||
return t.div({className: "CallTileView_error"}, t.view(new ErrorView(vm.errorViewModel, {inline: true})));
|
return t.div({className: "CallTileView_error"}, t.view(new ErrorView(vm.errorViewModel, {inline: true})));
|
||||||
}),
|
}),
|
||||||
t.div([
|
t.div([
|
||||||
vm => vm.label,
|
t.div({className: "CallTileView_title"}, vm => vm.title),
|
||||||
t.button({className: "CallTileView_join", hidden: vm => !vm.canJoin}, "Join"),
|
t.div({className: "CallTileView_subtitle"}, [
|
||||||
t.button({className: "CallTileView_leave", hidden: vm => !vm.canLeave}, "Leave")
|
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")
|
||||||
|
])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@ -38,9 +50,9 @@ export class CallTileView extends TemplateView<CallTile> {
|
|||||||
|
|
||||||
/* This is called by the parent ListView, which just has 1 listener for the whole list */
|
/* This is called by the parent ListView, which just has 1 listener for the whole list */
|
||||||
onClick(evt) {
|
onClick(evt) {
|
||||||
if (evt.target.className === "CallTileView_join") {
|
if (evt.target.classList.contains("CallTileView_join")) {
|
||||||
this.value.join();
|
this.value.join();
|
||||||
} else if (evt.target.className === "CallTileView_leave") {
|
} else if (evt.target.classList.contains("CallTileView_leave")) {
|
||||||
this.value.leave();
|
this.value.leave();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
52
src/utils/timeFormatting.ts
Normal file
52
src/utils/timeFormatting.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
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,
|
||||||
|
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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user