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

Show call length during a call #6700

Merged
merged 5 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
turt2live marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1891,7 +1891,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");
});
});