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

Labs feature: Early implementation of voice messages #5769

Merged
merged 18 commits into from
Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.13",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0",
Expand Down
5 changes: 3 additions & 2 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@
@import "./views/elements/_AddressSelector.scss";
@import "./views/elements/_AddressTile.scss";
@import "./views/elements/_DesktopBuildsNotice.scss";
@import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_DesktopCapturerSourcePicker.scss";
@import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss";
Expand Down Expand Up @@ -211,20 +211,21 @@
@import "./views/rooms/_SendMessageComposer.scss";
@import "./views/rooms/_Stickers.scss";
@import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_VoiceRecordComposerTile.scss";
@import "./views/rooms/_WhoIsTypingTile.scss";
@import "./views/settings/_AvatarSetting.scss";
@import "./views/settings/_CrossSigningPanel.scss";
@import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_E2eAdvancedPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_SpellCheckLanguages.scss";
@import "./views/settings/_IntegrationManager.scss";
@import "./views/settings/_Notifications.scss";
@import "./views/settings/_PhoneNumbers.scss";
@import "./views/settings/_ProfileSettings.scss";
@import "./views/settings/_SecureBackupPanel.scss";
@import "./views/settings/_SetIdServer.scss";
@import "./views/settings/_SetIntegrationManager.scss";
@import "./views/settings/_SpellCheckLanguages.scss";
@import "./views/settings/_UpdateCheckButton.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
Expand Down
5 changes: 5 additions & 0 deletions res/css/views/rooms/_BasicMessageComposer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ limitations under the License.
}
}
}

&.mx_BasicMessageComposer_input_disabled {
pointer-events: none;
cursor: not-allowed;
}
}

.mx_BasicMessageComposer_AutoCompleteWrapper {
Expand Down
4 changes: 4 additions & 0 deletions res/css/views/rooms/_MessageComposer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
}

.mx_MessageComposer_voiceMessage::before {
mask-image: url('$(res)/img/voip/mic-on-mask.svg');
}

.mx_MessageComposer_emoji::before {
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
}
Expand Down
36 changes: 36 additions & 0 deletions res/css/views/rooms/_VoiceRecordComposerTile.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.

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.
*/

.mx_VoiceRecordComposerTile_stop {
// 28px plus a 2px border makes this a 32px square (as intended)
width: 28px;
height: 28px;
border: 2px solid $voice-record-stop-border-color;
border-radius: 32px;
margin-right: 16px; // between us and the send button
position: relative;

&::after {
content: '';
width: 14px;
height: 14px;
position: absolute;
top: 7px;
left: 7px;
border-radius: 2px;
background-color: $voice-record-stop-symbol-color;
}
}
3 changes: 3 additions & 0 deletions res/img/voip/mic-on-mask.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions res/themes/legacy-light/css/_legacy-light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;

$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color;

$roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #fff;
Expand Down
3 changes: 3 additions & 0 deletions res/themes/light/css/_light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;

$voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color;

$roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #FFF;
Expand Down
2 changes: 2 additions & 0 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
import {SpaceStoreClass} from "../stores/SpaceStore";
import {VoiceRecorder} from "../voice/VoiceRecorder";

declare global {
interface Window {
Expand Down Expand Up @@ -70,6 +71,7 @@ declare global {
mxModalWidgetStore: ModalWidgetStore;
mxVoipUserMapper: VoipUserMapper;
mxSpaceStore: SpaceStoreClass;
mxVoiceRecorder: typeof VoiceRecorder;
}

interface Document {
Expand Down
4 changes: 4 additions & 0 deletions src/components/views/messages/MessageEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export default class MessageEvent extends React.Component {
'm.file': sdk.getComponent('messages.MFileBody'),
'm.audio': sdk.getComponent('messages.MAudioBody'),
'm.video': sdk.getComponent('messages.MVideoBody'),

// TODO: @@ TravisR: Use labs flag determination.
// MSC: https://github.com/matrix-org/matrix-doc/pull/2516
'org.matrix.msc2516.voice': sdk.getComponent('messages.MAudioBody'),
};
const evTypes = {
'm.sticker': sdk.getComponent('messages.MStickerBody'),
Expand Down
5 changes: 5 additions & 0 deletions src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ interface IProps {
placeholder?: string;
label?: string;
initialCaret?: DocumentOffset;
disabled?: boolean;

onChange?();
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
Expand Down Expand Up @@ -672,6 +673,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
});
const classes = classNames("mx_BasicMessageComposer_input", {
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,

// TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way.
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
});

const shortcuts = {
Expand Down Expand Up @@ -704,6 +708,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
aria-expanded={Boolean(this.state.autoComplete)}
aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
dir="auto"
aria-disabled={this.props.disabled}
/>
</div>);
}
Expand Down
29 changes: 25 additions & 4 deletions src/components/views/rooms/MessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import WidgetStore from "../../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";

function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
Expand Down Expand Up @@ -187,6 +188,7 @@ export default class MessageComposer extends React.Component {
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
isComposerEmpty: true,
haveRecording: false,
};
}

Expand Down Expand Up @@ -325,6 +327,10 @@ export default class MessageComposer extends React.Component {
});
}

onVoiceUpdate = (haveRecording: boolean) => {
this.setState({haveRecording});
};

render() {
const controls = [
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
Expand All @@ -346,17 +352,32 @@ export default class MessageComposer extends React.Component {
permalinkCreator={this.props.permalinkCreator}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
// TODO: @@ TravisR - Disabling the composer doesn't work
disabled={this.state.haveRecording}
/>,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);

if (!this.state.haveRecording) {
controls.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);
}

if (SettingsStore.getValue(UIFeature.Widgets) &&
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
!this.state.haveRecording) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}

if (!this.state.isComposerEmpty) {
if (SettingsStore.getValue("feature_voice_messages")) {
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
room={this.props.room}
onRecording={this.onVoiceUpdate} />);
}

if (!this.state.isComposerEmpty || this.state.haveRecording) {
controls.push(
<SendButton key="controls_send" onClick={this.sendMessage} />,
);
Expand Down
2 changes: 2 additions & 0 deletions src/components/views/rooms/SendMessageComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default class SendMessageComposer extends React.Component {
permalinkCreator: PropTypes.object.isRequired,
replyToEvent: PropTypes.object,
onChange: PropTypes.func,
disabled: PropTypes.bool,
};

static contextType = MatrixClientContext;
Expand Down Expand Up @@ -556,6 +557,7 @@ export default class SendMessageComposer extends React.Component {
label={this.props.placeholder}
placeholder={this.props.placeholder}
onPaste={this._onPaste}
disabled={this.props.disabled}
/>
</div>
);
Expand Down
88 changes: 88 additions & 0 deletions src/components/views/rooms/VoiceRecordComposerTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.

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 AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {_t} from "../../../languageHandler";
import React from "react";
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import classNames from "classnames";

interface IProps {
room: Room;
onRecording: (haveRecording: boolean) => void;
}

interface IState {
recorder?: VoiceRecorder;
}

export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);

this.state = {
recorder: null, // not recording by default
};
}

private onStartVoiceMessage = async () => {
turt2live marked this conversation as resolved.
Show resolved Hide resolved
// TODO: @@ TravisR: We do not want to auto-send on stop.
if (this.state.recorder) {
await this.state.recorder.stop();
const mxc = await this.state.recorder.upload();
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
body: "Voice message",
msgtype: "org.matrix.msc2516.voice",
url: mxc,
});
this.setState({recorder: null});
this.props.onRecording(false);
return;
}
const recorder = new VoiceRecorder(MatrixClientPeg.get());
await recorder.start();
this.props.onRecording(true);
// TODO: @@ TravisR: Run through EQ component
// recorder.frequencyData.onUpdate((freq) => {
// console.log('@@ UPDATE', freq);
// });
this.setState({recorder});
};

public render() {
const classes = classNames({
'mx_MessageComposer_button': !this.state.recorder,
'mx_MessageComposer_voiceMessage': !this.state.recorder,
'mx_VoiceRecordComposerTile_stop': !!this.state.recorder,
});

let tooltip = _t("Record a voice message");
if (!!this.state.recorder) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!!this.state.recorder) {
if (Boolean(this.state.recorder)) {

I think this is our nominally preferred way of doing this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it? I don't recall this, and haven't done this conversion in years

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough - I think this might be from kegan days. I have no preference personally.

// TODO: @@ TravisR: Change to match behaviour
tooltip = _t("Stop & send recording");
}

return (
<AccessibleTooltipButton
className={classes}
onClick={this.onStartVoiceMessage}
title={tooltip}
/>
);
}
}
3 changes: 3 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings",
"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.",
"Send and receive voice messages (in development)": "Send and receive voice messages (in development)",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
"New spinner design": "New spinner design",
Expand Down Expand Up @@ -1635,6 +1636,8 @@
"Invited by %(sender)s": "Invited by %(sender)s",
"Jump to first unread message.": "Jump to first unread message.",
"Mark all as read": "Mark all as read",
"Record a voice message": "Record a voice message",
"Stop & send recording": "Stop & send recording",
"Error updating main address": "Error updating main address",
"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.",
"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.",
Expand Down
6 changes: 6 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false,
controller: new ReloadOnChangeController(),
},
"feature_voice_messages": {
isFeature: true,
displayName: _td("Send and receive voice messages (in development)"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_latex_maths": {
isFeature: true,
displayName: _td("Render LaTeX maths in messages"),
Expand Down
Loading