diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 7998337e2e..156aac2eb8 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -111,7 +111,7 @@ export default class UserProvider extends AutocompleteProvider { // relies on the length of the entity === length of the text in the decoration. completion: user.rawDisplayName.replace(' (IRC)', ''), completionId: user.userId, - suffix: (selection.beginning && selection.start === 0) ? ': ' : ' ', + suffix: (selection.beginning && range.start === 0) ? ': ' : ' ', href: makeUserPermalink(user.userId), component: ( { - const active = marks.some(mark => mark.type === name) || blockType === 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'; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 5d6b12f339..1850601d8a 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -382,7 +382,8 @@ export default class MessageComposerInput extends React.Component { const quote = Block.create('block-quote'); if (this.state.isRichTextEnabled) { let change = editorState.change(); - if (editorState.anchorText.text === '' && editorState.anchorBlock.nodes.size === 1) { + const anchorText = editorState.anchorText; + if ((!anchorText || anchorText.text === '') && editorState.anchorBlock.nodes.size === 1) { // replace the current block rather than split the block change = change.replaceNodeByKey(editorState.anchorBlock.key, quote); } @@ -969,19 +970,21 @@ export default class MessageComposerInput extends React.Component { onPaste = (event: Event, change: Change, editor: Editor): Change => { const transfer = getEventTransfer(event); - if (transfer.type === "files") { - return this.props.onFilesPasted(transfer.files); - } - else if (transfer.type === "html") { - // FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means - // that we will silently discard nested blocks (e.g. nested lists) :( - const fragment = this.html.deserialize(transfer.html); - if (this.state.isRichTextEnabled) { - return change.insertFragment(fragment.document); - } - else { - return change.insertText(this.md.serialize(fragment)); + switch (transfer.type) { + case 'files': + return this.props.onFilesPasted(transfer.files); + case 'html': { + // FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means + // that we will silently discard nested blocks (e.g. nested lists) :( + const fragment = this.html.deserialize(transfer.html); + if (this.state.isRichTextEnabled) { + return change.insertFragment(fragment.document); + } else { + return change.insertText(this.md.serialize(fragment)); + } } + case 'text': + return change.insertText(transfer.text); } }; @@ -990,15 +993,30 @@ export default class MessageComposerInput extends React.Component { return change.insertText('\n'); } - if (this.state.editorState.blocks.some( - block => ['code', 'block-quote', 'list-item'].includes(block.type) - )) { - // allow the user to terminate blocks by hitting return rather than sending a msg + const editorState = this.state.editorState; + + const lastBlock = editorState.blocks.last(); + if (['code', 'block-quote', 'list-item'].includes(lastBlock.type)) { + const text = lastBlock.text; + if (text === '') { + // allow the user to cancel empty block by hitting return, useful in conjunction with below `inBlock` + return change + .setBlocks(DEFAULT_NODE) + .unwrapBlock('bulleted-list') + .unwrapBlock('numbered-list'); + } + + // TODO strip trailing lines from blockquotes/list entries + // the below code seemingly works but doesn't account for edge cases like return with caret not at end + /* const trailingNewlines = text.match(/\n*$/); + if (trailingNewlines && trailingNewlines[0]) { + remove trailing newlines at the end of this block before making a new one + return change.deleteBackward(trailingNewlines[0].length); + }*/ + return; } - const editorState = this.state.editorState; - let contentText; let contentHTML; @@ -1515,6 +1533,14 @@ export default class MessageComposerInput extends React.Component { mx_MessageComposer_input_error: this.state.someCompletions === false, }); + const isEmpty = this.state.editorState.document.isEmpty; + + let {placeholder} = this.props; + // XXX: workaround for placeholder being shown when there is a formatting block e.g blockquote but no text + if (isEmpty && this.state.editorState.startBlock.type !== DEFAULT_NODE) { + placeholder = undefined; + } + return (
@@ -1536,7 +1562,7 @@ export default class MessageComposerInput extends React.Component {
diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js index 0b22057f3c..ab2dbfedec 100644 --- a/src/stores/MessageComposerStore.js +++ b/src/stores/MessageComposerStore.js @@ -32,7 +32,13 @@ class MessageComposerStore { setEditorState(roomId: string, editorState: Value, richText: boolean) { localStorage.setItem(this._getKey(roomId), JSON.stringify({ - editor_state: editorState, + editor_state: editorState.toJSON({ + preserveSelection: true, + // XXX: re-hydrating history is not currently supported by fromJSON + // preserveHistory: true, + // XXX: this seems like a workaround for selection.isSet being based on anchorKey instead of anchorPath + preserveKeys: true, + }), rich_text: richText, })); }