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

Commit b68fabb

Browse files
authored
Merge pull request #5801 from matrix-org/travis/voice-messages/waveform
Show waveform and timer in voice messages
2 parents b089f3d + b154120 commit b68fabb

File tree

12 files changed

+407
-32
lines changed

12 files changed

+407
-32
lines changed

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@
247247
@import "./views/toasts/_AnalyticsToast.scss";
248248
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
249249
@import "./views/verification/_VerificationShowSas.scss";
250+
@import "./views/voice_messages/_Waveform.scss";
250251
@import "./views/voip/_CallContainer.scss";
251252
@import "./views/voip/_CallView.scss";
252253
@import "./views/voip/_DialPad.scss";

res/css/views/rooms/_VoiceRecordComposerTile.scss

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,43 @@ limitations under the License.
3434
background-color: $voice-record-stop-symbol-color;
3535
}
3636
}
37+
38+
.mx_VoiceRecordComposerTile_waveformContainer {
39+
padding: 5px;
40+
padding-right: 4px; // there's 1px from the waveform itself, so account for that
41+
padding-left: 15px; // +10px for the live circle, +5px for regular padding
42+
background-color: $voice-record-waveform-bg-color;
43+
border-radius: 12px;
44+
margin-right: 12px; // isolate from stop button
45+
46+
// Cheat at alignment a bit
47+
display: flex;
48+
align-items: center;
49+
50+
position: relative; // important for the live circle
51+
52+
color: $voice-record-waveform-fg-color;
53+
font-size: $font-14px;
54+
55+
&::before {
56+
// TODO: @@ TravisR: Animate
57+
content: '';
58+
background-color: $voice-record-live-circle-color;
59+
width: 10px;
60+
height: 10px;
61+
position: absolute;
62+
left: 8px;
63+
top: 16px; // vertically center
64+
border-radius: 10px;
65+
}
66+
67+
.mx_Waveform_bar {
68+
background-color: $voice-record-waveform-fg-color;
69+
}
70+
71+
.mx_Clock {
72+
padding-right: 8px; // isolate from waveform
73+
padding-left: 10px; // isolate from live circle
74+
width: 42px; // we're not using a monospace font, so fake it
75+
}
76+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_Waveform {
18+
position: relative;
19+
height: 30px; // tallest bar can only be 30px
20+
top: 1px; // because of our border trick (see below), we're off by 1px of aligntment
21+
22+
display: flex;
23+
align-items: center; // so the bars grow from the middle
24+
25+
overflow: hidden; // this is cheaper than a `max-height: calc(100% - 4px)` in the bar's CSS.
26+
27+
// A bar is meant to be a 2x2 circle when at zero height, and otherwise a 2px wide line
28+
// with rounded caps.
29+
.mx_Waveform_bar {
30+
width: 0; // 0px width means we'll end up using the border as our width
31+
border: 1px solid transparent; // transparent means we'll use the background colour
32+
border-radius: 2px; // rounded end caps, based on the border
33+
min-height: 0; // like the width, we'll rely on the border to give us height
34+
max-height: 100%; // this makes the `height: 42%` work on the element
35+
margin-left: 1px; // we want 2px between each bar, so 1px on either side for balance
36+
margin-right: 1px;
37+
38+
// background color is handled by the parent components
39+
}
40+
}

res/themes/legacy-light/css/_legacy-light.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ $groupFilterPanel-divider-color: $roomlist-header-color;
190190

191191
$voice-record-stop-border-color: #E3E8F0;
192192
$voice-record-stop-symbol-color: $warning-color;
193+
$voice-record-waveform-bg-color: #E3E8F0;
194+
$voice-record-waveform-fg-color: $muted-fg-color;
195+
$voice-record-live-circle-color: $warning-color;
193196

194197
$roomtile-preview-color: #9e9e9e;
195198
$roomtile-default-badge-bg-color: #61708b;

res/themes/light/css/_light.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ $groupFilterPanel-divider-color: $roomlist-header-color;
181181

182182
$voice-record-stop-border-color: #E3E8F0;
183183
$voice-record-stop-symbol-color: $warning-color;
184+
$voice-record-waveform-bg-color: #E3E8F0;
185+
$voice-record-waveform-fg-color: $muted-fg-color;
186+
$voice-record-live-circle-color: $warning-color;
184187

185188
$roomtile-preview-color: $secondary-fg-color;
186189
$roomtile-default-badge-bg-color: #61708b;

src/components/views/rooms/VoiceRecordComposerTile.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import {VoiceRecorder} from "../../../voice/VoiceRecorder";
2121
import {Room} from "matrix-js-sdk/src/models/room";
2222
import {MatrixClientPeg} from "../../../MatrixClientPeg";
2323
import classNames from "classnames";
24+
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
25+
import {replaceableComponent} from "../../../utils/replaceableComponent";
26+
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
2427

2528
interface IProps {
2629
room: Room;
@@ -31,6 +34,10 @@ interface IState {
3134
recorder?: VoiceRecorder;
3235
}
3336

37+
/**
38+
* Container tile for rendering the voice message recorder in the composer.
39+
*/
40+
@replaceableComponent("views.rooms.VoiceRecordComposerTile")
3441
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
3542
public constructor(props) {
3643
super(props);
@@ -57,13 +64,18 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
5764
const recorder = new VoiceRecorder(MatrixClientPeg.get());
5865
await recorder.start();
5966
this.props.onRecording(true);
60-
// TODO: @@ TravisR: Run through EQ component
61-
// recorder.frequencyData.onUpdate((freq) => {
62-
// console.log('@@ UPDATE', freq);
63-
// });
6467
this.setState({recorder});
6568
};
6669

70+
private renderWaveformArea() {
71+
if (!this.state.recorder) return null;
72+
73+
return <div className='mx_VoiceRecordComposerTile_waveformContainer'>
74+
<LiveRecordingClock recorder={this.state.recorder} />
75+
<LiveRecordingWaveform recorder={this.state.recorder} />
76+
</div>;
77+
}
78+
6779
public render() {
6880
const classes = classNames({
6981
'mx_MessageComposer_button': !this.state.recorder,
@@ -77,12 +89,13 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
7789
tooltip = _t("Stop & send recording");
7890
}
7991

80-
return (
92+
return (<>
93+
{this.renderWaveformArea()}
8194
<AccessibleTooltipButton
8295
className={classes}
8396
onClick={this.onStartStopVoiceMessage}
8497
title={tooltip}
8598
/>
86-
);
99+
</>);
87100
}
88101
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
import {replaceableComponent} from "../../../utils/replaceableComponent";
19+
20+
interface IProps {
21+
seconds: number;
22+
}
23+
24+
interface IState {
25+
}
26+
27+
/**
28+
* Simply converts seconds into minutes and seconds. Note that hours will not be
29+
* displayed, making it possible to see "82:29".
30+
*/
31+
@replaceableComponent("views.voice_messages.Clock")
32+
export default class Clock extends React.PureComponent<IProps, IState> {
33+
public constructor(props) {
34+
super(props);
35+
}
36+
37+
public render() {
38+
const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0');
39+
const seconds = Math.round(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis
40+
return <span className='mx_Clock'>{minutes}:{seconds}</span>;
41+
}
42+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
import {IRecordingUpdate, VoiceRecorder} from "../../../voice/VoiceRecorder";
19+
import {replaceableComponent} from "../../../utils/replaceableComponent";
20+
import Clock from "./Clock";
21+
22+
interface IProps {
23+
recorder: VoiceRecorder;
24+
}
25+
26+
interface IState {
27+
seconds: number;
28+
}
29+
30+
/**
31+
* A clock for a live recording.
32+
*/
33+
@replaceableComponent("views.voice_messages.LiveRecordingClock")
34+
export default class LiveRecordingClock extends React.Component<IProps, IState> {
35+
public constructor(props) {
36+
super(props);
37+
38+
this.state = {seconds: 0};
39+
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
40+
}
41+
42+
shouldComponentUpdate(nextProps: Readonly<IProps>, nextState: Readonly<IState>, nextContext: any): boolean {
43+
const currentFloor = Math.floor(this.state.seconds);
44+
const nextFloor = Math.floor(nextState.seconds);
45+
return currentFloor !== nextFloor;
46+
}
47+
48+
private onRecordingUpdate = (update: IRecordingUpdate) => {
49+
this.setState({seconds: update.timeSeconds});
50+
};
51+
52+
public render() {
53+
return <Clock seconds={this.state.seconds} />;
54+
}
55+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
import {IRecordingUpdate, VoiceRecorder} from "../../../voice/VoiceRecorder";
19+
import {replaceableComponent} from "../../../utils/replaceableComponent";
20+
import {arrayFastResample, arraySeed} from "../../../utils/arrays";
21+
import {percentageOf} from "../../../utils/numbers";
22+
import Waveform from "./Waveform";
23+
24+
interface IProps {
25+
recorder: VoiceRecorder;
26+
}
27+
28+
interface IState {
29+
heights: number[];
30+
}
31+
32+
const DOWNSAMPLE_TARGET = 35; // number of bars we want
33+
34+
/**
35+
* A waveform which shows the waveform of a live recording
36+
*/
37+
@replaceableComponent("views.voice_messages.LiveRecordingWaveform")
38+
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
39+
public constructor(props) {
40+
super(props);
41+
42+
this.state = {heights: arraySeed(0, DOWNSAMPLE_TARGET)};
43+
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
44+
}
45+
46+
private onRecordingUpdate = (update: IRecordingUpdate) => {
47+
// The waveform and the downsample target are pretty close, so we should be fine to
48+
// do this, despite the docs on arrayFastResample.
49+
const bars = arrayFastResample(Array.from(update.waveform), DOWNSAMPLE_TARGET);
50+
this.setState({
51+
// The incoming data is between zero and one, but typically even screaming into a
52+
// microphone won't send you over 0.6, so we artificially adjust the gain for the
53+
// waveform. This results in a slightly more cinematic/animated waveform for the
54+
// user.
55+
heights: bars.map(b => percentageOf(b, 0, 0.50)),
56+
});
57+
};
58+
59+
public render() {
60+
return <Waveform relHeights={this.state.heights} />;
61+
}
62+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
import {replaceableComponent} from "../../../utils/replaceableComponent";
19+
20+
interface IProps {
21+
relHeights: number[]; // relative heights (0-1)
22+
}
23+
24+
interface IState {
25+
}
26+
27+
/**
28+
* A simple waveform component. This renders bars (centered vertically) for each
29+
* height provided in the component properties. Updating the properties will update
30+
* the rendered waveform.
31+
*/
32+
@replaceableComponent("views.voice_messages.Waveform")
33+
export default class Waveform extends React.PureComponent<IProps, IState> {
34+
public constructor(props) {
35+
super(props);
36+
}
37+
38+
public render() {
39+
return <div className='mx_Waveform'>
40+
{this.props.relHeights.map((h, i) => {
41+
return <span key={i} style={{height: (h * 100) + '%'}} className='mx_Waveform_bar' />;
42+
})}
43+
</div>;
44+
}
45+
}

0 commit comments

Comments
 (0)