From 61bb0267a2b0821f8c4ea73dbc6152f19e27fa72 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 28 Nov 2022 20:32:32 -0500 Subject: [PATCH 1/3] 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'. --- src/DateUtils.ts | 43 ++++++++++++------- .../structures/LegacyCallEventGrouper.ts | 4 +- .../views/messages/LegacyCallEvent.tsx | 4 +- src/components/views/voip/CallDuration.tsx | 4 +- src/i18n/strings/en_EN.json | 4 ++ test/utils/DateUtils-test.ts | 17 ++++++++ 6 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/DateUtils.ts b/src/DateUtils.ts index 25495b0542f..e03dace2139 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -124,19 +124,6 @@ export function formatTime(date: Date, showTwelveHour = false): string { 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 { 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'); @@ -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 * Returns value in biggest possible unit (day, hour, min, second) * Rounds values up until unit threshold * 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 { if (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) }); } + +/** + * 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 }); +} diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 117abdd69e1..356ba5b501e 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -119,9 +119,9 @@ export default class LegacyCallEventGrouper extends EventEmitter { return Boolean(this.reject); } - public get duration(): Date { + public get duration(): number { if (!this.hangup || !this.selectAnswer) return; - return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime()); + return this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime(); } /** diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx index 4ab3ba00c30..28bc00f6537 100644 --- a/src/components/views/messages/LegacyCallEvent.tsx +++ b/src/components/views/messages/LegacyCallEvent.tsx @@ -28,7 +28,7 @@ import LegacyCallEventGrouper, { import AccessibleButton from '../elements/AccessibleButton'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; -import { formatCallTime } from "../../../DateUtils"; +import { formatPreciseDuration } from "../../../DateUtils"; import Clock from "../audio_messages/Clock"; const MAX_NON_NARROW_WIDTH = 450 / 70 * 100; @@ -175,7 +175,7 @@ export default class LegacyCallEvent extends React.PureComponent const duration = this.props.callEventGrouper.duration; let text = _t("Call ended"); if (duration) { - text += " • " + formatCallTime(duration); + text += " • " + formatPreciseDuration(duration); } return (
diff --git a/src/components/views/voip/CallDuration.tsx b/src/components/views/voip/CallDuration.tsx index 2965f6265ba..df59ba05d9c 100644 --- a/src/components/views/voip/CallDuration.tsx +++ b/src/components/views/voip/CallDuration.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { FC, useState, useEffect, memo } from "react"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; -import { formatCallTime } from "../../../DateUtils"; +import { formatPreciseDuration } from "../../../DateUtils"; interface CallDurationProps { delta: number; @@ -29,7 +29,7 @@ interface CallDurationProps { export const CallDuration: FC = memo(({ delta }) => { // Clock desync could lead to a negative duration, so just hide it if that happens if (delta <= 0) return null; - return
{ formatCallTime(new Date(delta)) }
; + return
{ formatPreciseDuration(delta) }
; }); interface GroupCallDurationProps { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 376133905dd..73084beaf93 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -56,6 +56,10 @@ "%(value)sh": "%(value)sh", "%(value)sm": "%(value)sm", "%(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", + "%(seconds)ss": "%(seconds)ss", "Identity server has no terms of service": "Identity server has no terms of service", "This action requires accessing the default identity 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 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.", diff --git a/test/utils/DateUtils-test.ts b/test/utils/DateUtils-test.ts index 2815b972d2a..da757b0f292 100644 --- a/test/utils/DateUtils-test.ts +++ b/test/utils/DateUtils-test.ts @@ -20,6 +20,7 @@ import { formatDuration, formatFullDateNoDayISO, formatTimeLeft, + formatPreciseDuration, } from "../../src/DateUtils"; 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", () => { it("should return ISO format", () => { expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z"); From 33131fe4ab51beaf42dde6f5017bf747e980f168 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 28 Nov 2022 23:22:17 -0500 Subject: [PATCH 2/3] Fix strict mode errors --- src/components/structures/LegacyCallEventGrouper.ts | 4 ++-- src/components/views/messages/LegacyCallEvent.tsx | 2 +- test/utils/DateUtils-test.ts | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 356ba5b501e..f6defe766f6 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -119,8 +119,8 @@ export default class LegacyCallEventGrouper extends EventEmitter { return Boolean(this.reject); } - public get duration(): number { - if (!this.hangup || !this.selectAnswer) return; + public get duration(): number | null { + if (!this.hangup || !this.selectAnswer) return null; return this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime(); } diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx index 28bc00f6537..5704895d18d 100644 --- a/src/components/views/messages/LegacyCallEvent.tsx +++ b/src/components/views/messages/LegacyCallEvent.tsx @@ -172,7 +172,7 @@ export default class LegacyCallEvent extends React.PureComponent // https://github.com/vector-im/riot-android/issues/2623 // Also the correct hangup code as of VoIP v1 (with underscore) // 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"); if (duration) { text += " • " + formatPreciseDuration(duration); diff --git a/test/utils/DateUtils-test.ts b/test/utils/DateUtils-test.ts index da757b0f292..55893e48d8e 100644 --- a/test/utils/DateUtils-test.ts +++ b/test/utils/DateUtils-test.ts @@ -125,7 +125,6 @@ describe("formatFullDateNoDayISO", () => { describe("formatTimeLeft", () => { it.each([ - [null, "0s left"], [0, "0s left"], [23, "23s left"], [60 + 23, "1m 23s left"], From 751441caa548a85c6d545f7088511afb7ac40b7e Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 28 Nov 2022 23:54:41 -0500 Subject: [PATCH 3/3] Fix strings --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 73084beaf93..c2f34afca99 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -59,7 +59,6 @@ "%(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", - "%(seconds)ss": "%(seconds)ss", "Identity server has no terms of service": "Identity server has no terms of service", "This action requires accessing the default identity 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 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.",