Merge branch 'develop' into bump-matrix-wysiwyg-to-0.8.0
This commit is contained in:
commit
90f87874a7
15 changed files with 212 additions and 117 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -1 +1,4 @@
|
||||||
* @matrix-org/element-web
|
* @matrix-org/element-web
|
||||||
|
/.github/workflows/** @matrix-org/element-web-app-team
|
||||||
|
/package.json @matrix-org/element-web-app-team
|
||||||
|
/yarn.lock @matrix-org/element-web-app-team
|
||||||
|
|
|
@ -164,9 +164,9 @@
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
"@types/parse5": "^6.0.0",
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "^17.0.49",
|
"@types/react": "17.0.49",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "^17.0.17",
|
"@types/react-dom": "17.0.17",
|
||||||
"@types/react-test-renderer": "^17.0.1",
|
"@types/react-test-renderer": "^17.0.1",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "^2.3.1",
|
"@types/sanitize-html": "^2.3.1",
|
||||||
|
|
|
@ -21,6 +21,10 @@ limitations under the License.
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
padding: $spacing-12;
|
padding: $spacing-12;
|
||||||
|
|
||||||
|
.mx_Clock {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody--pip {
|
.mx_VoiceBroadcastBody--pip {
|
||||||
|
@ -44,9 +48,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VoiceBroadcastBody_timerow {
|
.mx_VoiceBroadcastBody_timerow {
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-4;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
|
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
|
||||||
|
|
|
@ -124,20 +124,10 @@ export function formatTime(date: Date, showTwelveHour = false): string {
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
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 {
|
export function formatSeconds(inSeconds: number): string {
|
||||||
|
const isNegative = inSeconds < 0;
|
||||||
|
inSeconds = Math.abs(inSeconds);
|
||||||
|
|
||||||
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0).padStart(2, '0');
|
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 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');
|
const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0).padStart(2, '0');
|
||||||
|
@ -146,6 +136,10 @@ export function formatSeconds(inSeconds: number): string {
|
||||||
if (hours !== "00") output += `${hours}:`;
|
if (hours !== "00") output += `${hours}:`;
|
||||||
output += `${minutes}:${seconds}`;
|
output += `${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
if (isNegative) {
|
||||||
|
output = "-" + output;
|
||||||
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,15 +232,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
|
* Formats duration in ms to human readable string
|
||||||
* Returns value in biggest possible unit (day, hour, min, second)
|
* Returns value in biggest possible unit (day, hour, min, second)
|
||||||
* Rounds values up until unit threshold
|
* Rounds values up until unit threshold
|
||||||
* ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d
|
* 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 {
|
export function formatDuration(durationMs: number): string {
|
||||||
if (durationMs >= DAY_MS) {
|
if (durationMs >= DAY_MS) {
|
||||||
return _t('%(value)sd', { value: Math.round(durationMs / DAY_MS) });
|
return _t('%(value)sd', { value: Math.round(durationMs / DAY_MS) });
|
||||||
|
@ -259,3 +254,26 @@ export function formatDuration(durationMs: number): string {
|
||||||
}
|
}
|
||||||
return _t('%(value)ss', { value: Math.round(durationMs / 1000) });
|
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 });
|
||||||
|
}
|
||||||
|
|
|
@ -119,9 +119,9 @@ export default class LegacyCallEventGrouper extends EventEmitter {
|
||||||
return Boolean(this.reject);
|
return Boolean(this.reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get duration(): Date {
|
public get duration(): number | null {
|
||||||
if (!this.hangup || !this.selectAnswer) return;
|
if (!this.hangup || !this.selectAnswer) return null;
|
||||||
return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime());
|
return this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,7 +28,7 @@ import LegacyCallEventGrouper, {
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
|
||||||
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
import { formatCallTime } from "../../../DateUtils";
|
import { formatPreciseDuration } from "../../../DateUtils";
|
||||||
import Clock from "../audio_messages/Clock";
|
import Clock from "../audio_messages/Clock";
|
||||||
|
|
||||||
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
|
const MAX_NON_NARROW_WIDTH = 450 / 70 * 100;
|
||||||
|
@ -172,10 +172,10 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
// https://github.com/vector-im/riot-android/issues/2623
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
// Also, if we don't have a reason
|
// 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");
|
let text = _t("Call ended");
|
||||||
if (duration) {
|
if (duration) {
|
||||||
text += " • " + formatCallTime(duration);
|
text += " • " + formatPreciseDuration(duration);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_LegacyCallEvent_content">
|
<div className="mx_LegacyCallEvent_content">
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React, { FC, useState, useEffect, memo } from "react";
|
import React, { FC, useState, useEffect, memo } from "react";
|
||||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
|
||||||
import { formatCallTime } from "../../../DateUtils";
|
import { formatPreciseDuration } from "../../../DateUtils";
|
||||||
|
|
||||||
interface CallDurationProps {
|
interface CallDurationProps {
|
||||||
delta: number;
|
delta: number;
|
||||||
|
@ -29,7 +29,7 @@ interface CallDurationProps {
|
||||||
export const CallDuration: FC<CallDurationProps> = memo(({ delta }) => {
|
export const CallDuration: FC<CallDurationProps> = memo(({ delta }) => {
|
||||||
// Clock desync could lead to a negative duration, so just hide it if that happens
|
// Clock desync could lead to a negative duration, so just hide it if that happens
|
||||||
if (delta <= 0) return null;
|
if (delta <= 0) return null;
|
||||||
return <div className="mx_CallDuration">{ formatCallTime(new Date(delta)) }</div>;
|
return <div className="mx_CallDuration">{ formatPreciseDuration(delta) }</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
interface GroupCallDurationProps {
|
interface GroupCallDurationProps {
|
||||||
|
|
|
@ -56,6 +56,9 @@
|
||||||
"%(value)sh": "%(value)sh",
|
"%(value)sh": "%(value)sh",
|
||||||
"%(value)sm": "%(value)sm",
|
"%(value)sm": "%(value)sm",
|
||||||
"%(value)ss": "%(value)ss",
|
"%(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",
|
||||||
"Identity server has no terms of service": "Identity server has no terms of service",
|
"Identity server has no terms of service": "Identity server has no terms of service",
|
||||||
"This action requires accessing the default identity server <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 <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 <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 <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.",
|
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
|
||||||
|
|
|
@ -46,10 +46,9 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
||||||
playback,
|
playback,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
duration,
|
times,
|
||||||
liveness,
|
liveness,
|
||||||
playbackState,
|
playbackState,
|
||||||
position,
|
|
||||||
room,
|
room,
|
||||||
sender,
|
sender,
|
||||||
toggle,
|
toggle,
|
||||||
|
@ -94,7 +93,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
||||||
|
|
||||||
if (playbackState !== VoiceBroadcastPlaybackState.Stopped) {
|
if (playbackState !== VoiceBroadcastPlaybackState.Stopped) {
|
||||||
const onSeekBackwardButtonClick = () => {
|
const onSeekBackwardButtonClick = () => {
|
||||||
playback.skipTo(Math.max(0, position - SEEK_TIME));
|
playback.skipTo(Math.max(0, times.position - SEEK_TIME));
|
||||||
};
|
};
|
||||||
|
|
||||||
seekBackwardButton = <SeekButton
|
seekBackwardButton = <SeekButton
|
||||||
|
@ -104,7 +103,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const onSeekForwardButtonClick = () => {
|
const onSeekForwardButtonClick = () => {
|
||||||
playback.skipTo(Math.min(duration, position + SEEK_TIME));
|
playback.skipTo(Math.min(times.duration, times.position + SEEK_TIME));
|
||||||
};
|
};
|
||||||
|
|
||||||
seekForwardButton = <SeekButton
|
seekForwardButton = <SeekButton
|
||||||
|
@ -132,9 +131,10 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
||||||
{ control }
|
{ control }
|
||||||
{ seekForwardButton }
|
{ seekForwardButton }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_VoiceBroadcastBody_timerow">
|
|
||||||
<SeekBar playback={playback} />
|
<SeekBar playback={playback} />
|
||||||
<Clock seconds={duration} />
|
<div className="mx_VoiceBroadcastBody_timerow">
|
||||||
|
<Clock seconds={times.position} />
|
||||||
|
<Clock seconds={-times.timeLeft} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -40,18 +40,15 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [duration, setDuration] = useState(playback.durationSeconds);
|
const [times, setTimes] = useState({
|
||||||
|
duration: playback.durationSeconds,
|
||||||
|
position: playback.timeSeconds,
|
||||||
|
timeLeft: playback.timeLeftSeconds,
|
||||||
|
});
|
||||||
useTypedEventEmitter(
|
useTypedEventEmitter(
|
||||||
playback,
|
playback,
|
||||||
VoiceBroadcastPlaybackEvent.LengthChanged,
|
VoiceBroadcastPlaybackEvent.TimesChanged,
|
||||||
d => setDuration(d / 1000),
|
t => setTimes(t),
|
||||||
);
|
|
||||||
|
|
||||||
const [position, setPosition] = useState(playback.timeSeconds);
|
|
||||||
useTypedEventEmitter(
|
|
||||||
playback,
|
|
||||||
VoiceBroadcastPlaybackEvent.PositionChanged,
|
|
||||||
p => setPosition(p / 1000),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [liveness, setLiveness] = useState(playback.getLiveness());
|
const [liveness, setLiveness] = useState(playback.getLiveness());
|
||||||
|
@ -62,10 +59,9 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
duration,
|
times,
|
||||||
liveness: liveness,
|
liveness: liveness,
|
||||||
playbackState,
|
playbackState,
|
||||||
position,
|
|
||||||
room: room,
|
room: room,
|
||||||
sender: playback.infoEvent.sender,
|
sender: playback.infoEvent.sender,
|
||||||
toggle: playbackToggle,
|
toggle: playbackToggle,
|
||||||
|
|
|
@ -43,16 +43,20 @@ export enum VoiceBroadcastPlaybackState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VoiceBroadcastPlaybackEvent {
|
export enum VoiceBroadcastPlaybackEvent {
|
||||||
PositionChanged = "position_changed",
|
TimesChanged = "times_changed",
|
||||||
LengthChanged = "length_changed",
|
|
||||||
LivenessChanged = "liveness_changed",
|
LivenessChanged = "liveness_changed",
|
||||||
StateChanged = "state_changed",
|
StateChanged = "state_changed",
|
||||||
InfoStateChanged = "info_state_changed",
|
InfoStateChanged = "info_state_changed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VoiceBroadcastPlaybackTimes = {
|
||||||
|
duration: number;
|
||||||
|
position: number;
|
||||||
|
timeLeft: number;
|
||||||
|
};
|
||||||
|
|
||||||
interface EventMap {
|
interface EventMap {
|
||||||
[VoiceBroadcastPlaybackEvent.PositionChanged]: (position: number) => void;
|
[VoiceBroadcastPlaybackEvent.TimesChanged]: (times: VoiceBroadcastPlaybackTimes) => void;
|
||||||
[VoiceBroadcastPlaybackEvent.LengthChanged]: (length: number) => void;
|
|
||||||
[VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void;
|
[VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void;
|
||||||
[VoiceBroadcastPlaybackEvent.StateChanged]: (
|
[VoiceBroadcastPlaybackEvent.StateChanged]: (
|
||||||
state: VoiceBroadcastPlaybackState,
|
state: VoiceBroadcastPlaybackState,
|
||||||
|
@ -229,7 +233,7 @@ export class VoiceBroadcastPlayback
|
||||||
if (this.duration === duration) return;
|
if (this.duration === duration) return;
|
||||||
|
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.emit(VoiceBroadcastPlaybackEvent.LengthChanged, this.duration);
|
this.emitTimesChanged();
|
||||||
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,10 +241,21 @@ export class VoiceBroadcastPlayback
|
||||||
if (this.position === position) return;
|
if (this.position === position) return;
|
||||||
|
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.emit(VoiceBroadcastPlaybackEvent.PositionChanged, this.position);
|
this.emitTimesChanged();
|
||||||
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitTimesChanged(): void {
|
||||||
|
this.emit(
|
||||||
|
VoiceBroadcastPlaybackEvent.TimesChanged,
|
||||||
|
{
|
||||||
|
duration: this.durationSeconds,
|
||||||
|
position: this.timeSeconds,
|
||||||
|
timeLeft: this.timeLeftSeconds,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise<void> => {
|
private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise<void> => {
|
||||||
if (event !== this.currentlyPlaying) return;
|
if (event !== this.currentlyPlaying) return;
|
||||||
if (newState !== PlaybackState.Stopped) return;
|
if (newState !== PlaybackState.Stopped) return;
|
||||||
|
@ -337,6 +352,10 @@ export class VoiceBroadcastPlayback
|
||||||
return this.duration / 1000;
|
return this.duration / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get timeLeftSeconds(): number {
|
||||||
|
return Math.round(this.durationSeconds) - this.timeSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
public async skipTo(timeSeconds: number): Promise<void> {
|
public async skipTo(timeSeconds: number): Promise<void> {
|
||||||
const time = timeSeconds * 1000;
|
const time = timeSeconds * 1000;
|
||||||
const event = this.chunkEvents.findByTime(time);
|
const event = this.chunkEvents.findByTime(time);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
formatDuration,
|
formatDuration,
|
||||||
formatFullDateNoDayISO,
|
formatFullDateNoDayISO,
|
||||||
formatTimeLeft,
|
formatTimeLeft,
|
||||||
|
formatPreciseDuration,
|
||||||
} from "../../src/DateUtils";
|
} from "../../src/DateUtils";
|
||||||
import { REPEATABLE_DATE } from "../test-utils";
|
import { REPEATABLE_DATE } from "../test-utils";
|
||||||
|
|
||||||
|
@ -28,12 +29,14 @@ describe("formatSeconds", () => {
|
||||||
expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (55))).toBe("03:31:55");
|
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 * 0) + (55))).toBe("03:00:55");
|
||||||
expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (0))).toBe("03:31:00");
|
expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (0))).toBe("03:31:00");
|
||||||
|
expect(formatSeconds(-((60 * 60 * 3) + (60 * 31) + (0)))).toBe("-03:31:00");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly formats time without hours", () => {
|
it("correctly formats time without hours", () => {
|
||||||
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (55))).toBe("31:55");
|
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 * 0) + (55))).toBe("00:55");
|
||||||
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (0))).toBe("31:00");
|
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (0))).toBe("31:00");
|
||||||
|
expect(formatSeconds(-((60 * 60 * 0) + (60 * 31) + (0)))).toBe("-31:00");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -100,6 +103,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", () => {
|
describe("formatFullDateNoDayISO", () => {
|
||||||
it("should return ISO format", () => {
|
it("should return ISO format", () => {
|
||||||
expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z");
|
expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z");
|
||||||
|
@ -108,7 +127,6 @@ describe("formatFullDateNoDayISO", () => {
|
||||||
|
|
||||||
describe("formatTimeLeft", () => {
|
describe("formatTimeLeft", () => {
|
||||||
it.each([
|
it.each([
|
||||||
[null, "0s left"],
|
|
||||||
[0, "0s left"],
|
[0, "0s left"],
|
||||||
[23, "23s left"],
|
[23, "23s left"],
|
||||||
[60 + 23, "1m 23s left"],
|
[60 + 23, "1m 23s left"],
|
||||||
|
|
|
@ -42,6 +42,7 @@ jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
||||||
describe("VoiceBroadcastPlaybackBody", () => {
|
describe("VoiceBroadcastPlaybackBody", () => {
|
||||||
const userId = "@user:example.com";
|
const userId = "@user:example.com";
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
|
const duration = 23 * 60 + 42; // 23:42
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
let infoEvent: MatrixEvent;
|
let infoEvent: MatrixEvent;
|
||||||
let playback: VoiceBroadcastPlayback;
|
let playback: VoiceBroadcastPlayback;
|
||||||
|
@ -66,7 +67,7 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
||||||
jest.spyOn(playback, "getLiveness");
|
jest.spyOn(playback, "getLiveness");
|
||||||
jest.spyOn(playback, "getState");
|
jest.spyOn(playback, "getState");
|
||||||
jest.spyOn(playback, "skipTo");
|
jest.spyOn(playback, "skipTo");
|
||||||
jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(23 * 60 + 42); // 23:42
|
jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(duration);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when rendering a buffering voice broadcast", () => {
|
describe("when rendering a buffering voice broadcast", () => {
|
||||||
|
@ -95,7 +96,11 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
||||||
describe("and being in the middle of the playback", () => {
|
describe("and being in the middle of the playback", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
act(() => {
|
act(() => {
|
||||||
playback.emit(VoiceBroadcastPlaybackEvent.PositionChanged, 10 * 60 * 1000); // 10:00
|
playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, {
|
||||||
|
duration,
|
||||||
|
position: 10 * 60,
|
||||||
|
timeLeft: duration - 10 * 60,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -146,15 +151,20 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and the length updated", () => {
|
describe("and the times update", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
act(() => {
|
act(() => {
|
||||||
playback.emit(VoiceBroadcastPlaybackEvent.LengthChanged, 42000); // 00:42
|
playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, {
|
||||||
|
duration,
|
||||||
|
position: 5 * 60 + 13,
|
||||||
|
timeLeft: 7 * 60 + 5,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the new length", async () => {
|
it("should render the times", async () => {
|
||||||
expect(await screen.findByText("00:42")).toBeInTheDocument();
|
expect(await screen.findByText("05:13")).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText("-07:05")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -76,9 +76,6 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 0/not-live broadcast should
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VoiceBroadcastBody_timerow"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="mx_SeekBar"
|
class="mx_SeekBar"
|
||||||
max="1"
|
max="1"
|
||||||
|
@ -89,10 +86,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 0/not-live broadcast should
|
||||||
type="range"
|
type="range"
|
||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_VoiceBroadcastBody_timerow"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_Clock"
|
class="mx_Clock"
|
||||||
>
|
>
|
||||||
23:42
|
00:00
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mx_Clock"
|
||||||
|
>
|
||||||
|
-23:42
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -183,9 +188,6 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 1/live broadcast should ren
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VoiceBroadcastBody_timerow"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="mx_SeekBar"
|
class="mx_SeekBar"
|
||||||
max="1"
|
max="1"
|
||||||
|
@ -196,10 +198,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 1/live broadcast should ren
|
||||||
type="range"
|
type="range"
|
||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_VoiceBroadcastBody_timerow"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_Clock"
|
class="mx_Clock"
|
||||||
>
|
>
|
||||||
23:42
|
00:00
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mx_Clock"
|
||||||
|
>
|
||||||
|
-23:42
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -291,9 +301,6 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VoiceBroadcastBody_timerow"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="mx_SeekBar"
|
class="mx_SeekBar"
|
||||||
max="1"
|
max="1"
|
||||||
|
@ -304,10 +311,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
|
||||||
type="range"
|
type="range"
|
||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_VoiceBroadcastBody_timerow"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_Clock"
|
class="mx_Clock"
|
||||||
>
|
>
|
||||||
23:42
|
00:00
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mx_Clock"
|
||||||
|
>
|
||||||
|
-23:42
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -390,9 +405,6 @@ exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should re
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VoiceBroadcastBody_timerow"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="mx_SeekBar"
|
class="mx_SeekBar"
|
||||||
max="1"
|
max="1"
|
||||||
|
@ -403,10 +415,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should re
|
||||||
type="range"
|
type="range"
|
||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_VoiceBroadcastBody_timerow"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_Clock"
|
class="mx_Clock"
|
||||||
>
|
>
|
||||||
23:42
|
00:00
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mx_Clock"
|
||||||
|
>
|
||||||
|
-23:42
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -469,9 +489,6 @@ exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should re
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="mx_VoiceBroadcastBody_timerow"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
class="mx_SeekBar"
|
class="mx_SeekBar"
|
||||||
max="1"
|
max="1"
|
||||||
|
@ -482,10 +499,18 @@ exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should re
|
||||||
type="range"
|
type="range"
|
||||||
value="0"
|
value="0"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_VoiceBroadcastBody_timerow"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="mx_Clock"
|
class="mx_Clock"
|
||||||
>
|
>
|
||||||
23:42
|
00:00
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mx_Clock"
|
||||||
|
>
|
||||||
|
-23:42
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2500,7 +2500,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-dom@<18.0.0", "@types/react-dom@^17.0.17":
|
"@types/react-dom@17.0.17", "@types/react-dom@<18.0.0":
|
||||||
version "17.0.17"
|
version "17.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.17.tgz#2e3743277a793a96a99f1bf87614598289da68a1"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.17.tgz#2e3743277a793a96a99f1bf87614598289da68a1"
|
||||||
integrity sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==
|
integrity sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==
|
||||||
|
@ -2531,7 +2531,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^17", "@types/react@^17.0.49":
|
"@types/react@*", "@types/react@17.0.49", "@types/react@^17":
|
||||||
version "17.0.49"
|
version "17.0.49"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.49.tgz#df87ba4ca8b7942209c3dc655846724539dc1049"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.49.tgz#df87ba4ca8b7942209c3dc655846724539dc1049"
|
||||||
integrity sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==
|
integrity sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==
|
||||||
|
|
Loading…
Reference in a new issue