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

Commit 8587ec8

Browse files
authored
Merge pull request #5769 from matrix-org/travis/voice-messages/exp
Labs feature: Early implementation of voice messages
2 parents c5f145e + d929d48 commit 8587ec8

18 files changed

+378
-6
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
8484
"matrix-widget-api": "^0.1.0-beta.13",
8585
"minimist": "^1.2.5",
86+
"opus-recorder": "^8.0.3",
8687
"pako": "^2.0.3",
8788
"parse5": "^6.0.1",
8889
"png-chunks-extract": "^1.0.0",

res/css/_components.scss

+3-2
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@
111111
@import "./views/elements/_AddressSelector.scss";
112112
@import "./views/elements/_AddressTile.scss";
113113
@import "./views/elements/_DesktopBuildsNotice.scss";
114-
@import "./views/elements/_DirectorySearchBox.scss";
115114
@import "./views/elements/_DesktopCapturerSourcePicker.scss";
115+
@import "./views/elements/_DirectorySearchBox.scss";
116116
@import "./views/elements/_Dropdown.scss";
117117
@import "./views/elements/_EditableItemList.scss";
118118
@import "./views/elements/_ErrorBoundary.scss";
@@ -211,20 +211,21 @@
211211
@import "./views/rooms/_SendMessageComposer.scss";
212212
@import "./views/rooms/_Stickers.scss";
213213
@import "./views/rooms/_TopUnreadMessagesBar.scss";
214+
@import "./views/rooms/_VoiceRecordComposerTile.scss";
214215
@import "./views/rooms/_WhoIsTypingTile.scss";
215216
@import "./views/settings/_AvatarSetting.scss";
216217
@import "./views/settings/_CrossSigningPanel.scss";
217218
@import "./views/settings/_DevicesPanel.scss";
218219
@import "./views/settings/_E2eAdvancedPanel.scss";
219220
@import "./views/settings/_EmailAddresses.scss";
220-
@import "./views/settings/_SpellCheckLanguages.scss";
221221
@import "./views/settings/_IntegrationManager.scss";
222222
@import "./views/settings/_Notifications.scss";
223223
@import "./views/settings/_PhoneNumbers.scss";
224224
@import "./views/settings/_ProfileSettings.scss";
225225
@import "./views/settings/_SecureBackupPanel.scss";
226226
@import "./views/settings/_SetIdServer.scss";
227227
@import "./views/settings/_SetIntegrationManager.scss";
228+
@import "./views/settings/_SpellCheckLanguages.scss";
228229
@import "./views/settings/_UpdateCheckButton.scss";
229230
@import "./views/settings/tabs/_SettingsTab.scss";
230231
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";

res/css/views/rooms/_BasicMessageComposer.scss

+5
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ limitations under the License.
6666
}
6767
}
6868
}
69+
70+
&.mx_BasicMessageComposer_input_disabled {
71+
pointer-events: none;
72+
cursor: not-allowed;
73+
}
6974
}
7075

7176
.mx_BasicMessageComposer_AutoCompleteWrapper {

res/css/views/rooms/_MessageComposer.scss

+4
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ limitations under the License.
227227
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
228228
}
229229

230+
.mx_MessageComposer_voiceMessage::before {
231+
mask-image: url('$(res)/img/voip/mic-on-mask.svg');
232+
}
233+
230234
.mx_MessageComposer_emoji::before {
231235
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
232236
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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_VoiceRecordComposerTile_stop {
18+
// 28px plus a 2px border makes this a 32px square (as intended)
19+
width: 28px;
20+
height: 28px;
21+
border: 2px solid $voice-record-stop-border-color;
22+
border-radius: 32px;
23+
margin-right: 16px; // between us and the send button
24+
position: relative;
25+
26+
&::after {
27+
content: '';
28+
width: 14px;
29+
height: 14px;
30+
position: absolute;
31+
top: 7px;
32+
left: 7px;
33+
border-radius: 2px;
34+
background-color: $voice-record-stop-symbol-color;
35+
}
36+
}

res/img/voip/mic-on-mask.svg

+3
Loading

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

+3
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
189189
$groupFilterPanel-divider-color: $roomlist-header-color;
190190
$space-button-outline-color: #E3E8F0;
191191

192+
$voice-record-stop-border-color: #E3E8F0;
193+
$voice-record-stop-symbol-color: $warning-color;
194+
192195
$roomtile-preview-color: #9e9e9e;
193196
$roomtile-default-badge-bg-color: #61708b;
194197
$roomtile-selected-bg-color: #fff;

res/themes/light/css/_light.scss

+3
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
180180
$groupFilterPanel-divider-color: $roomlist-header-color;
181181
$space-button-outline-color: #E3E8F0;
182182

183+
$voice-record-stop-border-color: #E3E8F0;
184+
$voice-record-stop-symbol-color: $warning-color;
185+
183186
$roomtile-preview-color: $secondary-fg-color;
184187
$roomtile-default-badge-bg-color: #61708b;
185188
$roomtile-selected-bg-color: #FFF;

src/@types/global.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
3939
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
4040
import VoipUserMapper from "../VoipUserMapper";
4141
import {SpaceStoreClass} from "../stores/SpaceStore";
42+
import {VoiceRecorder} from "../voice/VoiceRecorder";
4243

4344
declare global {
4445
interface Window {
@@ -70,6 +71,7 @@ declare global {
7071
mxModalWidgetStore: ModalWidgetStore;
7172
mxVoipUserMapper: VoipUserMapper;
7273
mxSpaceStore: SpaceStoreClass;
74+
mxVoiceRecorder: typeof VoiceRecorder;
7375
}
7476

7577
interface Document {

src/components/views/messages/MessageEvent.js

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ export default class MessageEvent extends React.Component {
7171
'm.file': sdk.getComponent('messages.MFileBody'),
7272
'm.audio': sdk.getComponent('messages.MAudioBody'),
7373
'm.video': sdk.getComponent('messages.MVideoBody'),
74+
75+
// TODO: @@ TravisR: Use labs flag determination.
76+
// MSC: https://github.com/matrix-org/matrix-doc/pull/2516
77+
'org.matrix.msc2516.voice': sdk.getComponent('messages.MAudioBody'),
7478
};
7579
const evTypes = {
7680
'm.sticker': sdk.getComponent('messages.MStickerBody'),

src/components/views/rooms/BasicMessageComposer.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ interface IProps {
9393
placeholder?: string;
9494
label?: string;
9595
initialCaret?: DocumentOffset;
96+
disabled?: boolean;
9697

9798
onChange?();
9899
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
@@ -672,6 +673,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
672673
});
673674
const classes = classNames("mx_BasicMessageComposer_input", {
674675
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
676+
677+
// TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way.
678+
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
675679
});
676680

677681
const shortcuts = {
@@ -704,6 +708,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
704708
aria-expanded={Boolean(this.state.autoComplete)}
705709
aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
706710
dir="auto"
711+
aria-disabled={this.props.disabled}
707712
/>
708713
</div>);
709714
}

src/components/views/rooms/MessageComposer.js

+25-4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import WidgetStore from "../../../stores/WidgetStore";
3333
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
3434
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
3535
import {replaceableComponent} from "../../../utils/replaceableComponent";
36+
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
3637

3738
function ComposerAvatar(props) {
3839
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@@ -187,6 +188,7 @@ export default class MessageComposer extends React.Component {
187188
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
188189
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
189190
isComposerEmpty: true,
191+
haveRecording: false,
190192
};
191193
}
192194

@@ -325,6 +327,10 @@ export default class MessageComposer extends React.Component {
325327
});
326328
}
327329

330+
onVoiceUpdate = (haveRecording: boolean) => {
331+
this.setState({haveRecording});
332+
};
333+
328334
render() {
329335
const controls = [
330336
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
@@ -346,17 +352,32 @@ export default class MessageComposer extends React.Component {
346352
permalinkCreator={this.props.permalinkCreator}
347353
replyToEvent={this.props.replyToEvent}
348354
onChange={this.onChange}
355+
// TODO: @@ TravisR - Disabling the composer doesn't work
356+
disabled={this.state.haveRecording}
349357
/>,
350-
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
351-
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
352358
);
353359

360+
if (!this.state.haveRecording) {
361+
controls.push(
362+
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
363+
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
364+
);
365+
}
366+
354367
if (SettingsStore.getValue(UIFeature.Widgets) &&
355-
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
368+
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
369+
!this.state.haveRecording) {
356370
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
357371
}
358372

359-
if (!this.state.isComposerEmpty) {
373+
if (SettingsStore.getValue("feature_voice_messages")) {
374+
controls.push(<VoiceRecordComposerTile
375+
key="controls_voice_record"
376+
room={this.props.room}
377+
onRecording={this.onVoiceUpdate} />);
378+
}
379+
380+
if (!this.state.isComposerEmpty || this.state.haveRecording) {
360381
controls.push(
361382
<SendButton key="controls_send" onClick={this.sendMessage} />,
362383
);

src/components/views/rooms/SendMessageComposer.js

+2
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export default class SendMessageComposer extends React.Component {
120120
permalinkCreator: PropTypes.object.isRequired,
121121
replyToEvent: PropTypes.object,
122122
onChange: PropTypes.func,
123+
disabled: PropTypes.bool,
123124
};
124125

125126
static contextType = MatrixClientContext;
@@ -556,6 +557,7 @@ export default class SendMessageComposer extends React.Component {
556557
label={this.props.placeholder}
557558
placeholder={this.props.placeholder}
558559
onPaste={this._onPaste}
560+
disabled={this.props.disabled}
559561
/>
560562
</div>
561563
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
18+
import {_t} from "../../../languageHandler";
19+
import React from "react";
20+
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
21+
import {Room} from "matrix-js-sdk/src/models/room";
22+
import {MatrixClientPeg} from "../../../MatrixClientPeg";
23+
import classNames from "classnames";
24+
25+
interface IProps {
26+
room: Room;
27+
onRecording: (haveRecording: boolean) => void;
28+
}
29+
30+
interface IState {
31+
recorder?: VoiceRecorder;
32+
}
33+
34+
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
35+
public constructor(props) {
36+
super(props);
37+
38+
this.state = {
39+
recorder: null, // not recording by default
40+
};
41+
}
42+
43+
private onStartStopVoiceMessage = async () => {
44+
// TODO: @@ TravisR: We do not want to auto-send on stop.
45+
if (this.state.recorder) {
46+
await this.state.recorder.stop();
47+
const mxc = await this.state.recorder.upload();
48+
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
49+
body: "Voice message",
50+
msgtype: "org.matrix.msc2516.voice",
51+
url: mxc,
52+
});
53+
this.setState({recorder: null});
54+
this.props.onRecording(false);
55+
return;
56+
}
57+
const recorder = new VoiceRecorder(MatrixClientPeg.get());
58+
await recorder.start();
59+
this.props.onRecording(true);
60+
// TODO: @@ TravisR: Run through EQ component
61+
// recorder.frequencyData.onUpdate((freq) => {
62+
// console.log('@@ UPDATE', freq);
63+
// });
64+
this.setState({recorder});
65+
};
66+
67+
public render() {
68+
const classes = classNames({
69+
'mx_MessageComposer_button': !this.state.recorder,
70+
'mx_MessageComposer_voiceMessage': !this.state.recorder,
71+
'mx_VoiceRecordComposerTile_stop': !!this.state.recorder,
72+
});
73+
74+
let tooltip = _t("Record a voice message");
75+
if (!!this.state.recorder) {
76+
// TODO: @@ TravisR: Change to match behaviour
77+
tooltip = _t("Stop & send recording");
78+
}
79+
80+
return (
81+
<AccessibleTooltipButton
82+
className={classes}
83+
onClick={this.onStartStopVoiceMessage}
84+
title={tooltip}
85+
/>
86+
);
87+
}
88+
}

src/i18n/strings/en_EN.json

+3
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@
783783
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
784784
"Change notification settings": "Change notification settings",
785785
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
786+
"Send and receive voice messages (in development)": "Send and receive voice messages (in development)",
786787
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
787788
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
788789
"New spinner design": "New spinner design",
@@ -1636,6 +1637,8 @@
16361637
"Invited by %(sender)s": "Invited by %(sender)s",
16371638
"Jump to first unread message.": "Jump to first unread message.",
16381639
"Mark all as read": "Mark all as read",
1640+
"Record a voice message": "Record a voice message",
1641+
"Stop & send recording": "Stop & send recording",
16391642
"Error updating main address": "Error updating main address",
16401643
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.",
16411644
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.",

src/settings/Settings.ts

+6
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
128128
default: false,
129129
controller: new ReloadOnChangeController(),
130130
},
131+
"feature_voice_messages": {
132+
isFeature: true,
133+
displayName: _td("Send and receive voice messages (in development)"),
134+
supportedLevels: LEVELS_FEATURE,
135+
default: false,
136+
},
131137
"feature_latex_maths": {
132138
isFeature: true,
133139
displayName: _td("Render LaTeX maths in messages"),

0 commit comments

Comments
 (0)