Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #6700 from SimonBrandner/feature/call-timer/18566
Browse files Browse the repository at this point in the history
Show call length during a call
  • Loading branch information
dbkr authored Sep 3, 2021
2 parents b5abaff + f75b5f1 commit de01dcf
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 17 deletions.
12 changes: 12 additions & 0 deletions src/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/components/structures/CallEventGrouper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
export enum CallEventGrouperEvent {
StateChanged = "state_changed",
SilencedChanged = "silenced_changed",
LengthChanged = "length_changed",
}

const CONNECTING_STATES = [
Expand Down Expand Up @@ -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();
};
Expand All @@ -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 = () => {
Expand Down
12 changes: 4 additions & 8 deletions src/components/views/audio_messages/Clock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IProps, IState> {
export default class Clock extends React.Component<IProps> {
public constructor(props) {
super(props);
}

shouldComponentUpdate(nextProps: Readonly<IProps>, nextState: Readonly<IState>, nextContext: any): boolean {
shouldComponentUpdate(nextProps: Readonly<IProps>): 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 <span className='mx_Clock'>{ minutes }:{ seconds }</span>;
return <span className='mx_Clock'>{ formatSeconds(this.props.seconds) }</span>;
}
}
27 changes: 19 additions & 8 deletions src/components/views/messages/CallEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand All @@ -38,13 +39,9 @@ interface IState {
callState: CallState | CustomCallState;
silenced: boolean;
narrow: boolean;
length: number;
}

const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([
[CallState.Connected, _td("Connected")],
[CallState.Connecting, _td("Connecting")],
]);

export default class CallEvent extends React.PureComponent<IProps, IState> {
private wrapperElement = createRef<HTMLDivElement>();
private resizeObserver: ResizeObserver;
Expand All @@ -56,12 +53,14 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
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);
Expand All @@ -70,10 +69,15 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
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;
Expand Down Expand Up @@ -214,10 +218,17 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
</div>
);
}
if (Array.from(TEXTUAL_STATES.keys()).includes(state)) {
if (state === CallState.Connected) {
return (
<div className="mx_CallEvent_content">
<Clock seconds={this.state.length} />
</div>
);
}
if (state === CallState.Connecting) {
return (
<div className="mx_CallEvent_content">
{ TEXTUAL_STATES.get(state) }
{ _t("Connecting") }
</div>
);
}
Expand Down
1 change: 0 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions test/utils/DateUtils-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright 2021 Šimon Brandner <[email protected]>
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");
});
});

0 comments on commit de01dcf

Please sign in to comment.