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

Commit f6347d2

Browse files
authored
Show time left for voice broadcast recordings (#9564)
1 parent 962e8e0 commit f6347d2

22 files changed

+469
-145
lines changed

res/img/element-icons/Timer.svg

Lines changed: 3 additions & 0 deletions
Loading

src/DateUtils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,31 @@ export function formatSeconds(inSeconds: number): string {
149149
return output;
150150
}
151151

152+
export function formatTimeLeft(inSeconds: number): string {
153+
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0);
154+
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0);
155+
const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0);
156+
157+
if (hours !== "0") {
158+
return _t("%(hours)sh %(minutes)sm %(seconds)ss left", {
159+
hours,
160+
minutes,
161+
seconds,
162+
});
163+
}
164+
165+
if (minutes !== "0") {
166+
return _t("%(minutes)sm %(seconds)ss left", {
167+
minutes,
168+
seconds,
169+
});
170+
}
171+
172+
return _t("%(seconds)ss left", {
173+
seconds,
174+
});
175+
}
176+
152177
const MILLIS_IN_DAY = 86400000;
153178
function withinPast24Hours(prevDate: Date, nextDate: Date): boolean {
154179
return Math.abs(prevDate.getTime() - nextDate.getTime()) <= MILLIS_IN_DAY;

src/IConfigOptions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ export interface IConfigOptions {
182182
voice_broadcast?: {
183183
// length per voice chunk in seconds
184184
chunk_length?: number;
185+
// max voice broadcast length in seconds
186+
max_length?: number;
185187
};
186188

187189
user_notice?: {

src/SdkConfig.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export const DEFAULTS: IConfigOptions = {
4747
url: "https://element.io/get-started",
4848
},
4949
voice_broadcast: {
50-
chunk_length: 120, // two minutes
50+
chunk_length: 2 * 60, // two minutes
51+
max_length: 4 * 60 * 60, // four hours
5152
},
5253
};
5354

src/components/views/audio_messages/Clock.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,34 @@ import React, { HTMLProps } from "react";
1818

1919
import { formatSeconds } from "../../../DateUtils";
2020

21-
interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
21+
interface Props extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
2222
seconds: number;
23+
formatFn?: (seconds: number) => string;
2324
}
2425

2526
/**
26-
* Simply converts seconds into minutes and seconds. Note that hours will not be
27-
* displayed, making it possible to see "82:29".
27+
* Simply converts seconds using formatFn.
28+
* Defaulting to formatSeconds().
29+
* Note that in this case hours will not be displayed, making it possible to see "82:29".
2830
*/
29-
export default class Clock extends React.Component<IProps> {
30-
public constructor(props) {
31+
export default class Clock extends React.Component<Props> {
32+
public static defaultProps = {
33+
formatFn: formatSeconds,
34+
};
35+
36+
public constructor(props: Props) {
3137
super(props);
3238
}
3339

34-
public shouldComponentUpdate(nextProps: Readonly<IProps>): boolean {
40+
public shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
3541
const currentFloor = Math.floor(this.props.seconds);
3642
const nextFloor = Math.floor(nextProps.seconds);
3743
return currentFloor !== nextFloor;
3844
}
3945

4046
public render() {
4147
return <span aria-live={this.props["aria-live"]} role={this.props.role} className='mx_Clock'>
42-
{ formatSeconds(this.props.seconds) }
48+
{ this.props.formatFn(this.props.seconds) }
4349
</span>;
4450
}
4551
}

src/i18n/strings/en_EN.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
4949
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
5050
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
51+
"%(hours)sh %(minutes)sm %(seconds)ss left": "%(hours)sh %(minutes)sm %(seconds)ss left",
52+
"%(minutes)sm %(seconds)ss left": "%(minutes)sm %(seconds)ss left",
53+
"%(seconds)ss left": "%(seconds)ss left",
5154
"%(date)s at %(time)s": "%(date)s at %(time)s",
5255
"%(value)sd": "%(value)sd",
5356
"%(value)sh": "%(value)sh",
@@ -1886,7 +1889,6 @@
18861889
"The conversation continues here.": "The conversation continues here.",
18871890
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
18881891
"You do not have permission to post to this room": "You do not have permission to post to this room",
1889-
"%(seconds)ss left": "%(seconds)ss left",
18901892
"Send voice message": "Send voice message",
18911893
"Hide stickers": "Hide stickers",
18921894
"Sticker": "Sticker",

src/voice-broadcast/audio/VoiceBroadcastRecorder.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ import { Optional } from "matrix-events-sdk";
1818
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
1919

2020
import { getChunkLength } from "..";
21-
import { VoiceRecording } from "../../audio/VoiceRecording";
21+
import { IRecordingUpdate, VoiceRecording } from "../../audio/VoiceRecording";
2222
import { concat } from "../../utils/arrays";
2323
import { IDestroyable } from "../../utils/IDestroyable";
2424
import { Singleflight } from "../../utils/Singleflight";
2525

2626
export enum VoiceBroadcastRecorderEvent {
2727
ChunkRecorded = "chunk_recorded",
28+
CurrentChunkLengthUpdated = "current_chunk_length_updated",
2829
}
2930

3031
interface EventMap {
3132
[VoiceBroadcastRecorderEvent.ChunkRecorded]: (chunk: ChunkRecordedPayload) => void;
33+
[VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated]: (length: number) => void;
3234
}
3335

3436
export interface ChunkRecordedPayload {
@@ -46,8 +48,11 @@ export class VoiceBroadcastRecorder
4648
implements IDestroyable {
4749
private headers = new Uint8Array(0);
4850
private chunkBuffer = new Uint8Array(0);
51+
// position of the previous chunk in seconds
4952
private previousChunkEndTimePosition = 0;
5053
private pagesFromRecorderCount = 0;
54+
// current chunk length in seconds
55+
private currentChunkLength = 0;
5156

5257
public constructor(
5358
private voiceRecording: VoiceRecording,
@@ -58,7 +63,11 @@ export class VoiceBroadcastRecorder
5863
}
5964

6065
public async start(): Promise<void> {
61-
return this.voiceRecording.start();
66+
await this.voiceRecording.start();
67+
this.voiceRecording.liveData.onUpdate((data: IRecordingUpdate) => {
68+
this.setCurrentChunkLength(data.timeSeconds - this.previousChunkEndTimePosition);
69+
});
70+
return;
6271
}
6372

6473
/**
@@ -68,15 +77,25 @@ export class VoiceBroadcastRecorder
6877
await this.voiceRecording.stop();
6978
// forget about that call, so that we can stop it again later
7079
Singleflight.forgetAllFor(this.voiceRecording);
71-
return this.extractChunk();
80+
const chunk = this.extractChunk();
81+
this.currentChunkLength = 0;
82+
this.previousChunkEndTimePosition = 0;
83+
return chunk;
7284
}
7385

7486
public get contentType(): string {
7587
return this.voiceRecording.contentType;
7688
}
7789

78-
private get chunkLength(): number {
79-
return this.voiceRecording.recorderSeconds - this.previousChunkEndTimePosition;
90+
private setCurrentChunkLength(currentChunkLength: number): void {
91+
if (this.currentChunkLength === currentChunkLength) return;
92+
93+
this.currentChunkLength = currentChunkLength;
94+
this.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, currentChunkLength);
95+
}
96+
97+
public getCurrentChunkLength(): number {
98+
return this.currentChunkLength;
8099
}
81100

82101
private onDataAvailable = (data: ArrayBuffer): void => {
@@ -89,6 +108,7 @@ export class VoiceBroadcastRecorder
89108
return;
90109
}
91110

111+
this.setCurrentChunkLength(this.voiceRecording.recorderSeconds - this.previousChunkEndTimePosition);
92112
this.handleData(dataArray);
93113
};
94114

@@ -98,7 +118,7 @@ export class VoiceBroadcastRecorder
98118
}
99119

100120
private emitChunkIfTargetLengthReached(): void {
101-
if (this.chunkLength >= this.targetChunkLength) {
121+
if (this.getCurrentChunkLength() >= this.targetChunkLength) {
102122
this.emitAndResetChunk();
103123
}
104124
}
@@ -114,9 +134,10 @@ export class VoiceBroadcastRecorder
114134
const currentRecorderTime = this.voiceRecording.recorderSeconds;
115135
const payload: ChunkRecordedPayload = {
116136
buffer: concat(this.headers, this.chunkBuffer),
117-
length: this.chunkLength,
137+
length: this.getCurrentChunkLength(),
118138
};
119139
this.chunkBuffer = new Uint8Array(0);
140+
this.setCurrentChunkLength(0);
120141
this.previousChunkEndTimePosition = currentRecorderTime;
121142
return payload;
122143
}

src/voice-broadcast/components/atoms/VoiceBroadcastHeader.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,21 @@ import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
1717
import { LiveBadge } from "../..";
1818
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
1919
import { Icon as MicrophoneIcon } from "../../../../res/img/voip/call-view/mic-on.svg";
20+
import { Icon as TimerIcon } from "../../../../res/img/element-icons/Timer.svg";
2021
import { _t } from "../../../languageHandler";
2122
import RoomAvatar from "../../../components/views/avatars/RoomAvatar";
2223
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
2324
import { Icon as XIcon } from "../../../../res/img/element-icons/cancel-rounded.svg";
25+
import Clock from "../../../components/views/audio_messages/Clock";
26+
import { formatTimeLeft } from "../../../DateUtils";
2427

2528
interface VoiceBroadcastHeaderProps {
2629
live?: boolean;
2730
onCloseClick?: () => void;
2831
room: Room;
2932
sender: RoomMember;
3033
showBroadcast?: boolean;
34+
timeLeft?: number;
3135
showClose?: boolean;
3236
}
3337

@@ -38,6 +42,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
3842
sender,
3943
showBroadcast = false,
4044
showClose = false,
45+
timeLeft,
4146
}) => {
4247
const broadcast = showBroadcast
4348
? <div className="mx_VoiceBroadcastHeader_line">
@@ -54,6 +59,13 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
5459
</AccessibleButton>
5560
: null;
5661

62+
const timeLeftLine = timeLeft
63+
? <div className="mx_VoiceBroadcastHeader_line">
64+
<TimerIcon className="mx_Icon mx_Icon_16" />
65+
<Clock formatFn={formatTimeLeft} seconds={timeLeft} />
66+
</div>
67+
: null;
68+
5769
return <div className="mx_VoiceBroadcastHeader">
5870
<RoomAvatar room={room} width={32} height={32} />
5971
<div className="mx_VoiceBroadcastHeader_content">
@@ -64,6 +76,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
6476
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
6577
<span>{ sender.name }</span>
6678
</div>
79+
{ timeLeftLine }
6780
{ broadcast }
6881
</div>
6982
{ liveBadge }

src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface VoiceBroadcastRecordingPipProps {
3535
export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProps> = ({ recording }) => {
3636
const {
3737
live,
38+
timeLeft,
3839
recordingState,
3940
room,
4041
sender,
@@ -58,6 +59,7 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
5859
live={live}
5960
sender={sender}
6061
room={room}
62+
timeLeft={timeLeft}
6163
/>
6264
<hr className="mx_VoiceBroadcastBody_divider" />
6365
<div className="mx_VoiceBroadcastBody_controls">

src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =
6565
},
6666
);
6767

68+
const [timeLeft, setTimeLeft] = useState(recording.getTimeLeft());
69+
useTypedEventEmitter(
70+
recording,
71+
VoiceBroadcastRecordingEvent.TimeLeftChanged,
72+
setTimeLeft,
73+
);
74+
6875
const live = [
6976
VoiceBroadcastInfoState.Started,
7077
VoiceBroadcastInfoState.Paused,
@@ -73,6 +80,7 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =
7380

7481
return {
7582
live,
83+
timeLeft,
7684
recordingState,
7785
room,
7886
sender: recording.infoEvent.sender,

src/voice-broadcast/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export * from "./stores/VoiceBroadcastPreRecordingStore";
4141
export * from "./stores/VoiceBroadcastRecordingsStore";
4242
export * from "./utils/checkVoiceBroadcastPreConditions";
4343
export * from "./utils/getChunkLength";
44+
export * from "./utils/getMaxBroadcastLength";
4445
export * from "./utils/hasRoomLiveVoiceBroadcast";
4546
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";
4647
export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile";

0 commit comments

Comments
 (0)