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);
}