diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 531c4442c1..a11ebeff7b 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -78,7 +78,7 @@ limitations under the License. display: flex; flex-direction: column; min-height: 60px; - justify-content: center; + justify-content: start; align-items: flex-start; font-size: 14px; margin-right: 6px; @@ -86,9 +86,8 @@ limitations under the License. .mx_MessageComposer_editor { width: 100%; - flex: 1; max-height: 120px; - min-height: 21px; + min-height: 19px; overflow: auto; } @@ -106,6 +105,7 @@ limitations under the License. display: none; } +/* .mx_MessageComposer_input .DraftEditor-root { width: 100%; flex: 1; @@ -114,6 +114,7 @@ limitations under the License. min-height: 21px; overflow: auto; } +*/ .mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer { /* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */ diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index e52a8a677f..9a6970a376 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -38,28 +38,44 @@ class HistoryItem { this.format = format; } + static fromJSON(obj): HistoryItem { + return new HistoryItem( + Value.fromJSON(obj.value), + obj.format + ); + } + + toJSON(): Object { + return { + value: this.value.toJSON(), + format: this.format + }; + } + + // FIXME: rather than supporting storing history in either format, why don't we pick + // one canonical form? toValue(outputFormat: MessageFormat): Value { if (outputFormat === 'markdown') { if (this.format === 'rich') { // convert a rich formatted history entry to its MD equivalent - return Plain.deserialize(Md.serialize(value)); + return Plain.deserialize(Md.serialize(this.value)); // return ContentState.createFromText(RichText.stateToMarkdown(contentState)); } else if (this.format === 'markdown') { - return value; + return this.value; } } else if (outputFormat === 'rich') { if (this.format === 'markdown') { // convert MD formatted string to its rich equivalent. - return Md.deserialize(Plain.serialize(value)); + return Md.deserialize(Plain.serialize(this.value)); // return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML()); } else if (this.format === 'rich') { - return value; + return this.value; } } log.error("unknown format -> outputFormat conversion"); - return value; + return this.value; } } @@ -76,7 +92,7 @@ export default class ComposerHistoryManager { let item; for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { this.history.push( - Object.assign(new HistoryItem(), JSON.parse(item)), + HistoryItem.fromJSON(JSON.parse(item)) ); } this.lastIndex = this.currentIndex; @@ -86,7 +102,7 @@ export default class ComposerHistoryManager { const item = new HistoryItem(value, format); this.history.push(item); this.currentIndex = this.lastIndex + 1; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item)); + sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); } getItem(offset: number, format: MessageFormat): ?Value { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 53f7a6d474..4b950c429d 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent'; @@ -101,6 +102,7 @@ export default class MessageComposerInput extends React.Component { onInputStateChanged: PropTypes.func, }; +/* static getKeyBinding(ev: SyntheticKeyboardEvent): string { // Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and // importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not @@ -135,6 +137,7 @@ export default class MessageComposerInput extends React.Component { return null; } +*/ client: MatrixClient; autocomplete: Autocomplete; @@ -392,8 +395,7 @@ export default class MessageComposerInput extends React.Component { } } - // Called by Draft to change editor contents - onEditorContentChanged = (change: Change) => { + onChange = (change: Change) => { /* editorState = RichText.attachImmutableEntitiesToEmoji(editorState); @@ -557,17 +559,25 @@ export default class MessageComposerInput extends React.Component { } onKeyDown = (ev: Event, change: Change, editor: Editor) => { - if (ev.keyCode === KeyCode.ENTER) { - return this.handleReturn(ev); + switch (ev.keyCode) { + case KeyCode.ENTER: + return this.handleReturn(ev); + case KeyCode.UP: + return this.onVerticalArrow(ev, true); + case KeyCode.DOWN: + return this.onVerticalArrow(ev, false); + default: + // don't intercept it + return; } } handleKeyCommand = (command: string): boolean => { -/* if (command === 'toggle-mode') { this.enableRichtext(!this.state.isRichtextEnabled); return true; } +/* let newState: ?EditorState = null; // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. @@ -699,12 +709,10 @@ export default class MessageComposerInput extends React.Component { }; */ handleReturn = (ev) => { -/* if (ev.shiftKey) { - this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); - return true; + return; } - +/* const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState); if ( ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'] @@ -718,15 +726,12 @@ export default class MessageComposerInput extends React.Component { } */ const contentState = this.state.editorState; -/* - if (!contentState.hasText()) { - return true; - } -*/ let contentText = Plain.serialize(contentState); let contentHTML; + if (contentText === '') return true; + /* // Strip MD user (tab-completed) mentions to preserve plaintext mention behaviour. // We have to do this now as opposed to after calculating the contentText for MD @@ -914,41 +919,39 @@ export default class MessageComposerInput extends React.Component { return true; }; - onUpArrow = (e) => { - this.onVerticalArrow(e, true); - }; - - onDownArrow = (e) => { - this.onVerticalArrow(e, false); - }; - onVerticalArrow = (e, up) => { if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { return; } -/* // Select history only if we are not currently auto-completing if (this.autocomplete.state.completionList.length === 0) { - // Don't go back in history if we're in the middle of a multi-line message - const selection = this.state.editorState.getSelection(); - const blockKey = selection.getStartKey(); - const firstBlock = this.state.editorState.getCurrentContent().getFirstBlock(); - const lastBlock = this.state.editorState.getCurrentContent().getLastBlock(); - let canMoveUp = false; - let canMoveDown = false; - if (blockKey === firstBlock.getKey()) { - canMoveUp = selection.getStartOffset() === selection.getEndOffset() && - selection.getStartOffset() === 0; + // determine whether our cursor is at the top or bottom of the multiline + // input box by just looking at the position of the plain old DOM selection. + const selection = window.getSelection(); + const range = selection.getRangeAt(0); + const cursorRect = range.getBoundingClientRect(); + + const editorNode = ReactDOM.findDOMNode(this.refs.editor); + const editorRect = editorNode.getBoundingClientRect(); + + let navigateHistory = false; + if (up) { + let scrollCorrection = editorNode.scrollTop; + if (cursorRect.top - editorRect.top + scrollCorrection == 0) { + navigateHistory = true; + } + } + else { + let scrollCorrection = + editorNode.scrollHeight - editorNode.clientHeight - editorNode.scrollTop; + if (cursorRect.bottom - editorRect.bottom + scrollCorrection == 0) { + navigateHistory = true; + } } - if (blockKey === lastBlock.getKey()) { - canMoveDown = selection.getStartOffset() === selection.getEndOffset() && - selection.getStartOffset() === lastBlock.getText().length; - } - - if ((up && !canMoveUp) || (!up && !canMoveDown)) return; + if (!navigateHistory) return; const selected = this.selectHistory(up); if (selected) { @@ -959,10 +962,8 @@ export default class MessageComposerInput extends React.Component { this.moveAutocompleteSelection(up); e.preventDefault(); } -*/ }; -/* selectHistory = async (up) => { const delta = up ? -1 : 1; @@ -984,26 +985,19 @@ export default class MessageComposerInput extends React.Component { return; } - const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'rich' : 'markdown'); - if (!newContent) return false; - let editorState = EditorState.push( - this.state.editorState, - newContent, - 'insert-characters', - ); + let editorState = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'rich' : 'markdown'); // Move selection to the end of the selected history - let newSelection = SelectionState.createEmpty(newContent.getLastBlock().getKey()); - newSelection = newSelection.merge({ - focusOffset: newContent.getLastBlock().getLength(), - anchorOffset: newContent.getLastBlock().getLength(), - }); - editorState = EditorState.forceSelection(editorState, newSelection); + const change = editorState.change().collapseToEndOf(editorState.document); + // XXX: should we be calling this.onChange(change) now? + // we skip it for now given we know we're about to setState anyway + editorState = change.value; - this.setState({editorState}); + this.setState({ editorState }, ()=>{ + this.refs.editor.focus(); + }); return true; }; -*/ onTab = async (e) => { this.setState({ @@ -1236,7 +1230,7 @@ export default class MessageComposerInput extends React.Component { className="mx_MessageComposer_editor" placeholder={this.props.placeholder} value={this.state.editorState} - onChange={this.onEditorContentChanged} + onChange={this.onChange} onKeyDown={this.onKeyDown} /* blockStyleFn={MessageComposerInput.getBlockStyle}