diff --git a/src/DateUtils.ts b/src/DateUtils.ts index e8b81ca315..c81099b893 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -136,6 +136,18 @@ export function formatCallTime(delta: Date): string { 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'); + const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0).padStart(2, '0'); + + let output = ""; + if (hours !== "00") output += `${hours}:`; + output += `${minutes}:${seconds}`; + + return output; +} + const MILLIS_IN_DAY = 86400000; export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { if (!nextEventDate || !prevEventDate) { diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index b48bb32efe..7f0324029e 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -25,6 +25,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; export enum CallEventGrouperEvent { StateChanged = "state_changed", SilencedChanged = "silenced_changed", + LengthChanged = "length_changed", } const CONNECTING_STATES = [ @@ -113,6 +114,10 @@ export default class CallEventGrouper extends EventEmitter { this.emit(CallEventGrouperEvent.SilencedChanged, newState); }; + private onLengthChanged = (length: number): void => { + this.emit(CallEventGrouperEvent.LengthChanged, length); + }; + public answerCall = () => { this.call?.answer(); }; @@ -139,6 +144,7 @@ export default class CallEventGrouper extends EventEmitter { private setCallListeners() { if (!this.call) return; this.call.addListener(CallEvent.State, this.setState); + this.call.addListener(CallEvent.LengthChanged, this.onLengthChanged); } private setState = () => { diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index cb1a179f2e..69244cc5ad 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -15,34 +15,30 @@ limitations under the License. */ import React from "react"; +import { formatSeconds } from "../../../DateUtils"; import { replaceableComponent } from "../../../utils/replaceableComponent"; export interface IProps { seconds: number; } -interface IState { -} - /** * Simply converts seconds into minutes and seconds. Note that hours will not be * displayed, making it possible to see "82:29". */ @replaceableComponent("views.audio_messages.Clock") -export default class Clock extends React.Component { +export default class Clock extends React.Component { public constructor(props) { super(props); } - shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { + shouldComponentUpdate(nextProps: Readonly): boolean { const currentFloor = Math.floor(this.props.seconds); const nextFloor = Math.floor(nextProps.seconds); return currentFloor !== nextFloor; } public render() { - const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0'); - const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis - return { minutes }:{ seconds }; + return { formatSeconds(this.props.seconds) }; } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 8c9a3da060..5f514b8390 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { createRef } from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { _t, _td } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper'; import AccessibleButton from '../elements/AccessibleButton'; @@ -26,6 +26,7 @@ import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import classNames from 'classnames'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; import { formatCallTime } from "../../../DateUtils"; +import Clock from "../audio_messages/Clock"; const MAX_NON_NARROW_WIDTH = 450 / 70 * 100; @@ -38,13 +39,9 @@ interface IState { callState: CallState | CustomCallState; silenced: boolean; narrow: boolean; + length: number; } -const TEXTUAL_STATES: Map = new Map([ - [CallState.Connected, _td("Connected")], - [CallState.Connecting, _td("Connecting")], -]); - export default class CallEvent extends React.PureComponent { private wrapperElement = createRef(); private resizeObserver: ResizeObserver; @@ -56,12 +53,14 @@ export default class CallEvent extends React.PureComponent { callState: this.props.callEventGrouper.state, silenced: false, narrow: false, + length: 0, }; } componentDidMount() { this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged); + this.props.callEventGrouper.addListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged); this.resizeObserver = new ResizeObserver(this.resizeObserverCallback); this.resizeObserver.observe(this.wrapperElement.current); @@ -70,10 +69,15 @@ export default class CallEvent extends React.PureComponent { componentWillUnmount() { this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged); this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged); + this.props.callEventGrouper.removeListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged); this.resizeObserver.disconnect(); } + private onLengthChanged = (length: number): void => { + this.setState({ length }); + }; + private resizeObserverCallback = (entries: ResizeObserverEntry[]): void => { const wrapperElementEntry = entries.find((entry) => entry.target === this.wrapperElement.current); if (!wrapperElementEntry) return; @@ -214,10 +218,17 @@ export default class CallEvent extends React.PureComponent { ); } - if (Array.from(TEXTUAL_STATES.keys()).includes(state)) { + if (state === CallState.Connected) { return (
- { TEXTUAL_STATES.get(state) } + +
+ ); + } + if (state === CallState.Connecting) { + return ( +
+ { _t("Connecting") }
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ec3343de6a..6d8ded4748 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1893,7 +1893,6 @@ "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", "Compare emoji": "Compare emoji", - "Connected": "Connected", "Call declined": "Call declined", "Call back": "Call back", "No answer": "No answer", diff --git a/test/utils/DateUtils-test.ts b/test/utils/DateUtils-test.ts new file mode 100644 index 0000000000..ad0530c87e --- /dev/null +++ b/test/utils/DateUtils-test.ts @@ -0,0 +1,31 @@ +/* +Copyright 2021 Šimon Brandner + +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. +*/ + +import { formatSeconds } from "../../src/DateUtils"; + +describe("formatSeconds", () => { + it("correctly formats time with hours", () => { + expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (55))).toBe("03:31:55"); + expect(formatSeconds((60 * 60 * 3) + (60 * 0) + (55))).toBe("03:00:55"); + expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (0))).toBe("03:31:00"); + }); + + it("correctly formats time without hours", () => { + expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (55))).toBe("31:55"); + expect(formatSeconds((60 * 60 * 0) + (60 * 0) + (55))).toBe("00:55"); + expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (0))).toBe("31:00"); + }); +});