diff --git a/res/css/_components.scss b/res/css/_components.scss
index 34d6f8a900..d19d07132c 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -159,6 +159,7 @@
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SearchableEntityList.scss";
+@import "./views/rooms/_SendMessageComposer.scss";
@import "./views/rooms/_Stickers.scss";
@import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_WhoIsTypingTile.scss";
diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss
new file mode 100644
index 0000000000..7bb20a443b
--- /dev/null
+++ b/res/css/views/rooms/_SendMessageComposer.scss
@@ -0,0 +1,30 @@
+/*
+Copyright 2019 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_SendMessageComposer {
+ flex: 1;
+ min-height: 50px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .mx_BasicMessageComposer {
+ .mx_BasicMessageComposer_input {
+ padding: 3px 0;
+ }
+ }
+}
+
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index a14bac5a2a..022d45e60e 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -16,30 +16,18 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
-import { _t, _td } from '../../../languageHandler';
+import { _t } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import RoomViewStore from '../../../stores/RoomViewStore';
-import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../matrix-to';
import ContentMessages from '../../../ContentMessages';
import classNames from 'classnames';
import E2EIcon from './E2EIcon';
-const formatButtonList = [
- _td("bold"),
- _td("italic"),
- _td("deleted"),
- _td("underlined"),
- _td("inline-code"),
- _td("block-quote"),
- _td("bulleted-list"),
- _td("numbered-list"),
-];
-
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
return
@@ -115,28 +103,11 @@ HangupButton.propTypes = {
roomId: PropTypes.string.isRequired,
};
-function FormattingButton(props) {
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
- return
;
-}
-
-FormattingButton.propTypes = {
- showFormatting: PropTypes.bool.isRequired,
- onClickHandler: PropTypes.func.isRequired,
-};
-
class UploadButton extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
}
+
constructor(props, context) {
super(props, context);
this.onUploadClick = this.onUploadClick.bind(this);
@@ -193,24 +164,14 @@ class UploadButton extends React.Component {
export default class MessageComposer extends React.Component {
constructor(props, context) {
super(props, context);
- this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
- this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
- this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this);
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
- this.renderFormatBar = this.renderFormatBar.bind(this);
this.state = {
- inputState: {
- marks: [],
- blockType: null,
- isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
- },
- showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(),
@@ -257,6 +218,7 @@ export default class MessageComposer extends React.Component {
onEvent(event) {
if (event.getType() !== 'm.room.encryption') return;
if (event.getRoomId() !== this.props.room.roomId) return;
+ // TODO: put (encryption state??) in state
this.forceUpdate();
}
@@ -281,34 +243,12 @@ export default class MessageComposer extends React.Component {
this.setState({ isQuoting });
}
-
onInputStateChanged(inputState) {
// Merge the new input state with old to support partial updates
inputState = Object.assign({}, this.state.inputState, inputState);
this.setState({inputState});
}
- _onAutocompleteConfirm(range, completion) {
- if (this.messageComposerInput) {
- this.messageComposerInput.setDisplayedCompletion(range, completion);
- }
- }
-
- onFormatButtonClicked(name, event) {
- event.preventDefault();
- this.messageComposerInput.onFormatButtonClicked(name, event);
- }
-
- onToggleFormattingClicked() {
- SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
- this.setState({showFormatting: !this.state.showFormatting});
- }
-
- onToggleMarkdownClicked(e) {
- e.preventDefault(); // don't steal focus from the editor!
- this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled);
- }
-
_onTombstoneClick(ev) {
ev.preventDefault();
@@ -355,47 +295,6 @@ export default class MessageComposer extends React.Component {
}
}
- renderFormatBar() {
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
- const {marks, blockType} = this.state.inputState;
- const formatButtons = formatButtonList.map((name) => {
- // special-case to match the md serializer and the special-case in MessageComposerInput.js
- const markName = name === 'inline-code' ? 'code' : name;
- const active = marks.some(mark => mark.type === markName) || blockType === name;
- const suffix = active ? '-on' : '';
- const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
- const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
- return (
-
})
- );
- });
-
- return (
-
-
- { formatButtons }
-
-
-
-
-
- );
- }
-
render() {
const controls = [
this.state.me ?
: null,
@@ -409,23 +308,16 @@ export default class MessageComposer extends React.Component {
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
- const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
- const showFormattingButton = this.state.inputState.isRichTextEnabled;
+ const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer");
const callInProgress = this.props.callState && this.props.callState !== 'ended';
controls.push(
-
this.messageComposerInput = c}
key="controls_input"
room={this.props.room}
placeholder={this.renderPlaceholderText()}
- onInputStateChanged={this.onInputStateChanged}
permalinkCreator={this.props.permalinkCreator} />,
- showFormattingButton ? :
- null,
,
,
callInProgress ? : null,
@@ -461,8 +353,6 @@ export default class MessageComposer extends React.Component {
);
}
- const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled;
-
const wrapperClasses = classNames({
mx_MessageComposer_wrapper: true,
mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
@@ -474,7 +364,6 @@ export default class MessageComposer extends React.Component {
{ controls }
- { showFormatBar ? this.renderFormatBar() : null }
);
}
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
new file mode 100644
index 0000000000..50a09eb894
--- /dev/null
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -0,0 +1,105 @@
+/*
+Copyright 2019 New Vector Ltd
+Copyright 2019 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 React from 'react';
+import PropTypes from 'prop-types';
+import dis from '../../../dispatcher';
+import EditorModel from '../../../editor/model';
+import {getCaretOffsetAndText} from '../../../editor/dom';
+import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
+import {PartCreator} from '../../../editor/parts';
+import EditorStateTransfer from '../../../utils/EditorStateTransfer';
+import {MatrixClient} from 'matrix-js-sdk';
+import BasicMessageComposer from "./BasicMessageComposer";
+
+function createMessageContent(model, editedEvent) {
+ const body = textSerialize(model);
+ const content = {
+ msgtype: "m.text",
+ body,
+ };
+ const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: false});
+ if (formattedBody) {
+ content.format = "org.matrix.custom.html";
+ content.formatted_body = formattedBody;
+ }
+ return content;
+}
+
+export default class SendMessageComposer extends React.Component {
+ static propTypes = {
+ // the message event being edited
+ editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
+ };
+
+ static contextTypes = {
+ matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ this.model = null;
+ this._editorRef = null;
+ }
+
+ _setEditorRef = ref => {
+ this._editorRef = ref;
+ };
+
+ _onKeyDown = (event) => {
+ if (event.metaKey || event.altKey || event.shiftKey) {
+ return;
+ }
+ if (event.key === "Enter") {
+ this._sendMessage();
+ event.preventDefault();
+ }
+ }
+
+ _sendMessage = () => {
+ const {roomId} = this.props.room;
+ this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model));
+ this.model.reset([]);
+ dis.dispatch({action: 'focus_composer'});
+ }
+
+ componentWillUnmount() {
+ const sel = document.getSelection();
+ const {caret} = getCaretOffsetAndText(this._editorRef, sel);
+ const parts = this.model.serializeParts();
+ this.props.editState.setEditorState(caret, parts);
+ }
+
+ componentWillMount() {
+ const partCreator = new PartCreator(this.props.room, this.context.matrixClient);
+ this.model = new EditorModel([], partCreator);
+ }
+
+ render() {
+ //