Show day counts in call durations (#9641)

* Show day counts in call durations

Previously call durations over a day long would be truncated, for example displaying as '2h 0m 0s' instead of '1d 2h 0m 0s'.

* Fix strict mode errors

* Fix strings

Co-authored-by: Travis Ralston <travisr@matrix.org>
This commit is contained in:
Robin 2022-11-29 16:21:51 -05:00 committed by GitHub
parent 440f76c3e8
commit 69e03860a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 55 additions and 25 deletions

View file

@ -124,19 +124,6 @@ export function formatTime(date: Date, showTwelveHour = false): string {
return pad(date.getHours()) + ':' + pad(date.getMinutes()); return pad(date.getHours()) + ':' + pad(date.getMinutes());
} }
export function formatCallTime(delta: Date): string {
const hours = delta.getUTCHours();
const minutes = delta.getUTCMinutes();
const seconds = delta.getUTCSeconds();
let output = "";
if (hours) output += `${hours}h `;
if (minutes || output) output += `${minutes}m `;
if (seconds || output) output += `${seconds}s`;
return output;
}
export function formatSeconds(inSeconds: number): string { export function formatSeconds(inSeconds: number): string {
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0).padStart(2, '0'); const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0).padStart(2, '0');
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0).padStart(2, '0'); const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0).padStart(2, '0');
@ -238,15 +225,16 @@ export function formatRelativeTime(date: Date, showTwelveHour = false): string {
} }
} }
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const DAY_MS = HOUR_MS * 24;
/** /**
* Formats duration in ms to human readable string * Formats duration in ms to human readable string
* Returns value in biggest possible unit (day, hour, min, second) * Returns value in biggest possible unit (day, hour, min, second)
* Rounds values up until unit threshold * Rounds values up until unit threshold
* ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d * ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d
*/ */
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const DAY_MS = HOUR_MS * 24;
export function formatDuration(durationMs: number): string { export function formatDuration(durationMs: number): string {
if (durationMs >= DAY_MS) { if (durationMs >= DAY_MS) {
return _t('%(value)sd', { value: Math.round(durationMs / DAY_MS) }); return _t('%(value)sd', { value: Math.round(durationMs / DAY_MS) });
@ -259,3 +247,26 @@ export function formatDuration(durationMs: number): string {
} }
return _t('%(value)ss', { value: Math.round(durationMs / 1000) }); return _t('%(value)ss', { value: Math.round(durationMs / 1000) });
} }
/**
* Formats duration in ms to human readable string
* Returns precise value down to the nearest second
* ie. 23:13:57 -> 23h 13m 57s, 44:56:56 -> 1d 20h 56m 56s
*/
export function formatPreciseDuration(durationMs: number): string {
const days = Math.floor(durationMs / DAY_MS);
const hours = Math.floor((durationMs % DAY_MS) / HOUR_MS);
const minutes = Math.floor((durationMs % HOUR_MS) / MINUTE_MS);
const seconds = Math.floor((durationMs % MINUTE_MS) / 1000);
if (days > 0) {
return _t('%(days)sd %(hours)sh %(minutes)sm %(seconds)ss', { days, hours, minutes, seconds });
}
if (hours > 0) {
return _t('%(hours)sh %(minutes)sm %(seconds)ss', { hours, minutes, seconds });
}
if (minutes > 0) {
return _t('%(minutes)sm %(seconds)ss', { minutes, seconds });
}
return _t('%(value)ss', { value: seconds });
}

View file

@ -119,9 +119,9 @@ export default class LegacyCallEventGrouper extends EventEmitter {
return Boolean(this.reject); return Boolean(this.reject);
} }
public get duration(): Date { public get duration(): number | null {
if (!this.hangup || !this.selectAnswer) return; if (!this.hangup || !this.selectAnswer) return null;
return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime()); return this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime();
} }
/** /**

View file

@ -28,7 +28,7 @@ import LegacyCallEventGrouper, {
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { formatCallTime } from "../../../DateUtils"; import { formatPreciseDuration } from "../../../DateUtils";
import Clock from "../audio_messages/Clock"; import Clock from "../audio_messages/Clock";
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100; const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
@ -172,10 +172,10 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
// https://github.com/vector-im/riot-android/issues/2623 // https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore) // Also the correct hangup code as of VoIP v1 (with underscore)
// Also, if we don't have a reason // Also, if we don't have a reason
const duration = this.props.callEventGrouper.duration; const duration = this.props.callEventGrouper.duration!;
let text = _t("Call ended"); let text = _t("Call ended");
if (duration) { if (duration) {
text += " • " + formatCallTime(duration); text += " • " + formatPreciseDuration(duration);
} }
return ( return (
<div className="mx_LegacyCallEvent_content"> <div className="mx_LegacyCallEvent_content">

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { FC, useState, useEffect, memo } from "react"; import React, { FC, useState, useEffect, memo } from "react";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
import { formatCallTime } from "../../../DateUtils"; import { formatPreciseDuration } from "../../../DateUtils";
interface CallDurationProps { interface CallDurationProps {
delta: number; delta: number;
@ -29,7 +29,7 @@ interface CallDurationProps {
export const CallDuration: FC<CallDurationProps> = memo(({ delta }) => { export const CallDuration: FC<CallDurationProps> = memo(({ delta }) => {
// Clock desync could lead to a negative duration, so just hide it if that happens // Clock desync could lead to a negative duration, so just hide it if that happens
if (delta <= 0) return null; if (delta <= 0) return null;
return <div className="mx_CallDuration">{ formatCallTime(new Date(delta)) }</div>; return <div className="mx_CallDuration">{ formatPreciseDuration(delta) }</div>;
}); });
interface GroupCallDurationProps { interface GroupCallDurationProps {

View file

@ -56,6 +56,9 @@
"%(value)sh": "%(value)sh", "%(value)sh": "%(value)sh",
"%(value)sm": "%(value)sm", "%(value)sm": "%(value)sm",
"%(value)ss": "%(value)ss", "%(value)ss": "%(value)ss",
"%(days)sd %(hours)sh %(minutes)sm %(seconds)ss": "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss",
"%(hours)sh %(minutes)sm %(seconds)ss": "%(hours)sh %(minutes)sm %(seconds)ss",
"%(minutes)sm %(seconds)ss": "%(minutes)sm %(seconds)ss",
"Identity server has no terms of service": "Identity server has no terms of service", "Identity server has no terms of service": "Identity server has no terms of service",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.", "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",

View file

@ -20,6 +20,7 @@ import {
formatDuration, formatDuration,
formatFullDateNoDayISO, formatFullDateNoDayISO,
formatTimeLeft, formatTimeLeft,
formatPreciseDuration,
} from "../../src/DateUtils"; } from "../../src/DateUtils";
import { REPEATABLE_DATE } from "../test-utils"; import { REPEATABLE_DATE } from "../test-utils";
@ -100,6 +101,22 @@ describe('formatDuration()', () => {
}); });
}); });
describe("formatPreciseDuration", () => {
const MINUTE_MS = 1000 * 60;
const HOUR_MS = MINUTE_MS * 60;
const DAY_MS = HOUR_MS * 24;
it.each<[string, string, number]>([
['3 days, 6 hours, 48 minutes, 59 seconds', '3d 6h 48m 59s', 3 * DAY_MS + 6 * HOUR_MS + 48 * MINUTE_MS + 59000],
['6 hours, 48 minutes, 59 seconds', '6h 48m 59s', 6 * HOUR_MS + 48 * MINUTE_MS + 59000],
['48 minutes, 59 seconds', '48m 59s', 48 * MINUTE_MS + 59000],
['59 seconds', '59s', 59000],
['0 seconds', '0s', 0],
])('%s formats to %s', (_description, expectedResult, input) => {
expect(formatPreciseDuration(input)).toEqual(expectedResult);
});
});
describe("formatFullDateNoDayISO", () => { describe("formatFullDateNoDayISO", () => {
it("should return ISO format", () => { it("should return ISO format", () => {
expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z"); expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z");
@ -108,7 +125,6 @@ describe("formatFullDateNoDayISO", () => {
describe("formatTimeLeft", () => { describe("formatTimeLeft", () => {
it.each([ it.each([
[null, "0s left"],
[0, "0s left"], [0, "0s left"],
[23, "23s left"], [23, "23s left"],
[60 + 23, "1m 23s left"], [60 + 23, "1m 23s left"],