From b5d32d92f31a9d65f02ae048c55360aef9e58296 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 15 Mar 2021 22:16:58 -0600 Subject: [PATCH] Wire up a simple record button --- .../views/rooms/_BasicMessageComposer.scss | 5 ++ res/css/views/rooms/_MessageComposer.scss | 4 + res/img/voip/mic-on-mask.svg | 3 + .../views/rooms/BasicMessageComposer.tsx | 3 + src/components/views/rooms/MessageComposer.js | 31 ++++++-- .../views/rooms/SendMessageComposer.js | 2 + .../views/rooms/VoiceRecordComposerTile.tsx | 74 +++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + src/voice/VoiceRecorder.ts | 12 +++ 9 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 res/img/voip/mic-on-mask.svg create mode 100644 src/components/views/rooms/VoiceRecordComposerTile.tsx diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e126e523a6..4f58c08617 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -66,6 +66,11 @@ limitations under the License. } } } + + &.mx_BasicMessageComposer_input_disabled { + pointer-events: none; + cursor: not-allowed; + } } .mx_BasicMessageComposer_AutoCompleteWrapper { diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index dea1b58741..e6c0cc3f46 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -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'); } diff --git a/res/img/voip/mic-on-mask.svg b/res/img/voip/mic-on-mask.svg new file mode 100644 index 0000000000..418316b164 --- /dev/null +++ b/res/img/voip/mic-on-mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 5ab2b82a32..223a7a5395 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -93,6 +93,7 @@ interface IProps { placeholder?: string; label?: string; initialCaret?: DocumentOffset; + disabled?: boolean; onChange?(); onPaste?(event: ClipboardEvent, model: EditorModel): boolean; @@ -672,6 +673,7 @@ export default class BasicMessageEditor extends React.Component }); const classes = classNames("mx_BasicMessageComposer_input", { "mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar, + "mx_BasicMessageComposer_input_disabled": this.props.disabled, }); const shortcuts = { @@ -704,6 +706,7 @@ export default class BasicMessageEditor extends React.Component aria-expanded={Boolean(this.state.autoComplete)} aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined} dir="auto" + aria-disabled={this.props.disabled} /> ); } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ccf097c4fd..2efda53bc2 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -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'); @@ -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, }; } @@ -325,6 +327,10 @@ export default class MessageComposer extends React.Component { }); } + onVoiceUpdate = (haveRecording: boolean) => { + this.setState({haveRecording}); + }; + render() { const controls = [ this.state.me ? : null, @@ -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} + /> ); + if (!this.state.haveRecording) { + controls.push( + , + , + ); + } + if (SettingsStore.getValue(UIFeature.Widgets) && - SettingsStore.getValue("MessageComposerInput.showStickersButton")) { + SettingsStore.getValue("MessageComposerInput.showStickersButton") && + !this.state.haveRecording) { controls.push(); } - if (!this.state.isComposerEmpty) { + if (SettingsStore.getValue("feature_voice_messages")) { + controls.push(); + } + + if (!this.state.isComposerEmpty || this.state.haveRecording) { controls.push( , ); diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index ba3076c07d..aed1bb36fe 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -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; @@ -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} /> ); diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx new file mode 100644 index 0000000000..9ba3764524 --- /dev/null +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -0,0 +1,74 @@ +/* +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"; + +interface IProps { + room: Room; + onRecording: (haveRecording: boolean) => void; +} + +interface IState { + recorder?: VoiceRecorder; +} + +export default class VoiceRecordComposerTile extends React.PureComponent { + public constructor(props) { + super(props); + + this.state = { + recorder: null, // not recording by default + }; + } + + private onStartVoiceMessage = async () => { + 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: "m.audio", // TODO + 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: Run through EQ component + recorder.rawData.onUpdate((frame) => { + console.log('@@ FRAME', frame); + }); + this.setState({recorder}); + }; + + public render() { + return ( + + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1a66e0cfee..4654cb1f99 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1636,6 +1636,7 @@ "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", "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.", diff --git a/src/voice/VoiceRecorder.ts b/src/voice/VoiceRecorder.ts index 2764d94174..df6621d00b 100644 --- a/src/voice/VoiceRecorder.ts +++ b/src/voice/VoiceRecorder.ts @@ -19,6 +19,7 @@ import encoderPath from 'opus-recorder/dist/encoderWorker.min.js'; import {MatrixClient} from "matrix-js-sdk/src/client"; import CallMediaHandler from "../CallMediaHandler"; import {sleep} from "../utils/promise"; +import {SimpleObservable} from "matrix-widget-api"; export class VoiceRecorder { private recorder = new Recorder({ @@ -34,6 +35,7 @@ export class VoiceRecorder { private buffer = new Uint8Array(0); private mxc: string; private recording = false; + private observable: SimpleObservable; public constructor(private client: MatrixClient) { this.recorder.ondataavailable = (a: ArrayBuffer) => { @@ -44,9 +46,15 @@ export class VoiceRecorder { newBuf.set(this.buffer, 0); newBuf.set(buf, this.buffer.length); this.buffer = newBuf; + this.observable.update(buf); // send the frame over the observable }; } + public get rawData(): SimpleObservable { + if (!this.recording) throw new Error("No observable when not recording"); + return this.observable; + } + public get isSupported(): boolean { return !!Recorder.isRecordingSupported(); } @@ -69,6 +77,10 @@ export class VoiceRecorder { if (this.recording) { throw new Error("Recording already in progress"); } + if (this.observable) { + this.observable.close(); + } + this.observable = new SimpleObservable(); return this.recorder.start().then(() => this.recording = true); }