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() { + //
+ //
+ // + return ( +
+ +
+ ); + } +}