Merge pull request #6700 from SimonBrandner/feature/call-timer/18566
Show call length during a call
This commit is contained in:
commit
de01dcfd26
6 changed files with 72 additions and 17 deletions
|
@ -136,6 +136,18 @@ export function formatCallTime(delta: Date): string {
|
||||||
return output;
|
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;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||||
if (!nextEventDate || !prevEventDate) {
|
if (!nextEventDate || !prevEventDate) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
export enum CallEventGrouperEvent {
|
export enum CallEventGrouperEvent {
|
||||||
StateChanged = "state_changed",
|
StateChanged = "state_changed",
|
||||||
SilencedChanged = "silenced_changed",
|
SilencedChanged = "silenced_changed",
|
||||||
|
LengthChanged = "length_changed",
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONNECTING_STATES = [
|
const CONNECTING_STATES = [
|
||||||
|
@ -113,6 +114,10 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
|
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onLengthChanged = (length: number): void => {
|
||||||
|
this.emit(CallEventGrouperEvent.LengthChanged, length);
|
||||||
|
};
|
||||||
|
|
||||||
public answerCall = () => {
|
public answerCall = () => {
|
||||||
this.call?.answer();
|
this.call?.answer();
|
||||||
};
|
};
|
||||||
|
@ -139,6 +144,7 @@ export default class CallEventGrouper extends EventEmitter {
|
||||||
private setCallListeners() {
|
private setCallListeners() {
|
||||||
if (!this.call) return;
|
if (!this.call) return;
|
||||||
this.call.addListener(CallEvent.State, this.setState);
|
this.call.addListener(CallEvent.State, this.setState);
|
||||||
|
this.call.addListener(CallEvent.LengthChanged, this.onLengthChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setState = () => {
|
private setState = () => {
|
||||||
|
|
|
@ -15,34 +15,30 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { formatSeconds } from "../../../DateUtils";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
seconds: number;
|
seconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simply converts seconds into minutes and seconds. Note that hours will not be
|
* Simply converts seconds into minutes and seconds. Note that hours will not be
|
||||||
* displayed, making it possible to see "82:29".
|
* displayed, making it possible to see "82:29".
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.audio_messages.Clock")
|
@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) {
|
public constructor(props) {
|
||||||
super(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 currentFloor = Math.floor(this.props.seconds);
|
||||||
const nextFloor = Math.floor(nextProps.seconds);
|
const nextFloor = Math.floor(nextProps.seconds);
|
||||||
return currentFloor !== nextFloor;
|
return currentFloor !== nextFloor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0');
|
return <span className='mx_Clock'>{ formatSeconds(this.props.seconds) }</span>;
|
||||||
const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis
|
|
||||||
return <span className='mx_Clock'>{ minutes }:{ seconds }</span>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper';
|
import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -26,6 +26,7 @@ import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
import { formatCallTime } from "../../../DateUtils";
|
import { formatCallTime } from "../../../DateUtils";
|
||||||
|
import Clock from "../audio_messages/Clock";
|
||||||
|
|
||||||
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
|
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
|
||||||
|
|
||||||
|
@ -38,13 +39,9 @@ interface IState {
|
||||||
callState: CallState | CustomCallState;
|
callState: CallState | CustomCallState;
|
||||||
silenced: boolean;
|
silenced: boolean;
|
||||||
narrow: 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> {
|
export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
private wrapperElement = createRef<HTMLDivElement>();
|
private wrapperElement = createRef<HTMLDivElement>();
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
@ -56,12 +53,14 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
callState: this.props.callEventGrouper.state,
|
callState: this.props.callEventGrouper.state,
|
||||||
silenced: false,
|
silenced: false,
|
||||||
narrow: false,
|
narrow: false,
|
||||||
|
length: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||||
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||||
|
this.props.callEventGrouper.addListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
||||||
this.resizeObserver.observe(this.wrapperElement.current);
|
this.resizeObserver.observe(this.wrapperElement.current);
|
||||||
|
@ -70,10 +69,15 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
|
||||||
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
|
||||||
|
this.props.callEventGrouper.removeListener(CallEventGrouperEvent.LengthChanged, this.onLengthChanged);
|
||||||
|
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onLengthChanged = (length: number): void => {
|
||||||
|
this.setState({ length });
|
||||||
|
};
|
||||||
|
|
||||||
private resizeObserverCallback = (entries: ResizeObserverEntry[]): void => {
|
private resizeObserverCallback = (entries: ResizeObserverEntry[]): void => {
|
||||||
const wrapperElementEntry = entries.find((entry) => entry.target === this.wrapperElement.current);
|
const wrapperElementEntry = entries.find((entry) => entry.target === this.wrapperElement.current);
|
||||||
if (!wrapperElementEntry) return;
|
if (!wrapperElementEntry) return;
|
||||||
|
@ -214,10 +218,17 @@ export default class CallEvent extends React.PureComponent<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (Array.from(TEXTUAL_STATES.keys()).includes(state)) {
|
if (state === CallState.Connected) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallEvent_content">
|
<div className="mx_CallEvent_content">
|
||||||
{ TEXTUAL_STATES.get(state) }
|
<Clock seconds={this.state.length} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state === CallState.Connecting) {
|
||||||
|
return (
|
||||||
|
<div className="mx_CallEvent_content">
|
||||||
|
{ _t("Connecting") }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1893,7 +1893,6 @@
|
||||||
"You cancelled verification.": "You cancelled verification.",
|
"You cancelled verification.": "You cancelled verification.",
|
||||||
"Verification cancelled": "Verification cancelled",
|
"Verification cancelled": "Verification cancelled",
|
||||||
"Compare emoji": "Compare emoji",
|
"Compare emoji": "Compare emoji",
|
||||||
"Connected": "Connected",
|
|
||||||
"Call declined": "Call declined",
|
"Call declined": "Call declined",
|
||||||
"Call back": "Call back",
|
"Call back": "Call back",
|
||||||
"No answer": "No answer",
|
"No answer": "No answer",
|
||||||
|
|
31
test/utils/DateUtils-test.ts
Normal file
31
test/utils/DateUtils-test.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue