- if (i !== contentDiv.children.length - 1) {
- contentHTML += '
';
- }
- } else if (element.tagName.toLowerCase() === 'pre') {
- // Replace "
\n" with "\n" within `
` tags because the
is - // redundant. This is a workaround for a bug in draft-js-export-html: - // https://github.com/sstur/draft-js-export-html/issues/62 - contentHTML += '' + - element.innerHTML.replace(/'; - } else { - const temp = document.createElement('div'); - temp.appendChild(element.cloneNode(true)); - contentHTML += temp.innerHTML; - } - } - - return contentHTML; -} -*/ - /* * Given an untrusted HTML string, return a React node with an sanitized version * of that HTML. diff --git a/src/Markdown.js b/src/Markdown.js index 9d9a8621c9..dc0d5962fd 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -180,14 +180,6 @@ export default class Markdown { if (is_multi_line(node) && node.next) this.lit('\n\n'); }; - // convert MD links into console-friendly ' < http://foo >' style links - // ...except given this function never gets called with links, it's useless. - // renderer.link = function(node, entering) { - // if (!entering) { - // this.lit(` < ${node.destination} >`); - // } - // }; - return renderer.render(this.parsed); } } diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index b3c20a713c..7f91676cc3 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -29,9 +29,9 @@ import NotifProvider from './NotifProvider'; import Promise from 'bluebird'; export type SelectionRange = { - beginning: boolean, - start: number, - end: number + beginning: boolean, // whether the selection is in the first block of the editor or not + start: number, // byte offset relative to the start anchor of the current editor selection. + end: number, // byte offset relative to the end anchor of the current editor selection. }; export type Completion = { diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 156aac2eb8..7998337e2e 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 && range.start === 0) ? ': ' : ' ', + suffix: (selection.beginning && selection.start === 0) ? ': ' : ' ', href: makeUserPermalink(user.userId), component: (
\n/g, '\n').trim() + - '{ if (obj.object !== 'inline') return; switch (obj.type) { @@ -288,9 +296,6 @@ export default class MessageComposerInput extends React.Component { } ] }); - - this.suppressAutoComplete = false; - this.direction = ''; } /* @@ -298,7 +303,8 @@ export default class MessageComposerInput extends React.Component { * - whether we've got rich text mode enabled * - contentState was passed in */ - createEditorState(richText: boolean, editorState: ?Value): Value { + createEditorState(richText: boolean, // eslint-disable-line no-unused-vars + editorState: ?Value): Value { if (editorState instanceof Value) { return editorState; } @@ -371,7 +377,6 @@ export default class MessageComposerInput extends React.Component { let fragmentChange = fragment.change(); fragmentChange.moveToRangeOf(fragment.document) .wrapBlock(quote); - //.setBlocks('block-quote'); // FIXME: handle pills and use commonmark rather than md-serialize const md = this.md.serialize(fragmentChange.value); @@ -459,6 +464,7 @@ export default class MessageComposerInput extends React.Component { if (this.direction !== '') { const focusedNode = editorState.focusInline || editorState.focusText; if (focusedNode.isVoid) { + // XXX: does this work in RTL? const edge = this.direction === 'Previous' ? 'End' : 'Start'; if (editorState.isCollapsed) { change = change[`collapseTo${ edge }Of${ this.direction }Text`](); @@ -478,13 +484,6 @@ export default class MessageComposerInput extends React.Component { this.onFinishedTyping(); } - /* - // XXX: what was this ever doing? - if (!state.hasOwnProperty('originalEditorState')) { - state.originalEditorState = null; - } - */ - if (editorState.startText !== null) { const text = editorState.startText.text; const currentStartOffset = editorState.startOffset; @@ -512,9 +511,7 @@ export default class MessageComposerInput extends React.Component { } // emojioneify any emoji - - // XXX: is getTextsAsArray a private API? - editorState.document.getTextsAsArray().forEach(node => { + editorState.document.getTexts().forEach(node => { if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) { let match; while ((match = EMOJI_REGEX.exec(node.text)) !== null) { @@ -546,36 +543,6 @@ export default class MessageComposerInput extends React.Component { editorState = change.value; } -/* - const currentBlock = editorState.getSelection().getStartKey(); - const currentSelection = editorState.getSelection(); - const currentStartOffset = editorState.getSelection().getStartOffset(); - - const block = editorState.getCurrentContent().getBlockForKey(currentBlock); - const text = block.getText(); - - const entityBeforeCurrentOffset = block.getEntityAt(currentStartOffset - 1); - const entityAtCurrentOffset = block.getEntityAt(currentStartOffset); - - // If the cursor is on the boundary between an entity and a non-entity and the - // text before the cursor has whitespace at the end, set the entity state of the - // character before the cursor (the whitespace) to null. This allows the user to - // stop editing the link. - if (entityBeforeCurrentOffset && !entityAtCurrentOffset && - /\s$/.test(text.slice(0, currentStartOffset))) { - editorState = RichUtils.toggleLink( - editorState, - currentSelection.merge({ - anchorOffset: currentStartOffset - 1, - focusOffset: currentStartOffset, - }), - null, - ); - // Reset selection - editorState = EditorState.forceSelection(editorState, currentSelection); - } -*/ - if (this.props.onInputStateChanged && editorState.blocks.size > 0) { let blockType = editorState.blocks.first().type; // console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks); @@ -605,7 +572,6 @@ export default class MessageComposerInput extends React.Component { editor_state: editorState, }); - /* Since a modification was made, set originalEditorState to null, since newState is now our original */ this.setState({ editorState, originalEditorState: originalEditorState || null @@ -683,7 +649,7 @@ export default class MessageComposerInput extends React.Component { hasMark = type => { const { editorState } = this.state - return editorState.activeMarks.some(mark => mark.type == type) + return editorState.activeMarks.some(mark => mark.type === type) }; /** @@ -695,7 +661,7 @@ export default class MessageComposerInput extends React.Component { hasBlock = type => { const { editorState } = this.state - return editorState.blocks.some(node => node.type == type) + return editorState.blocks.some(node => node.type === type) }; onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => { @@ -828,7 +794,7 @@ export default class MessageComposerInput extends React.Component { // Handle the extra wrapping required for list buttons. const isList = this.hasBlock('list-item'); const isType = editorState.blocks.some(block => { - return !!document.getClosest(block.key, parent => parent.type == type); + return !!document.getClosest(block.key, parent => parent.type === type); }); if (isList && isType) { @@ -839,7 +805,7 @@ export default class MessageComposerInput extends React.Component { } else if (isList) { change .unwrapBlock( - type == 'bulleted-list' ? 'numbered-list' : 'bulleted-list' + type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list' ) .wrapBlock(type); } else { @@ -1009,7 +975,7 @@ export default class MessageComposerInput extends React.Component { let contentHTML; // only look for commands if the first block contains simple unformatted text - // i.e. no pills or rich-text formatting. + // i.e. no pills or rich-text formatting and begins with a /. let cmd, commandText; const firstChild = editorState.document.nodes.get(0); const firstGrandChild = firstChild && firstChild.nodes.get(0); @@ -1090,8 +1056,8 @@ export default class MessageComposerInput extends React.Component { // didn't contain any formatting in the first place... contentText = mdWithPills.toPlaintext(); } else { - // to avoid ugliness clients which can't parse HTML we don't send pills - // in the plaintext body. + // to avoid ugliness on clients which ignore the HTML body we don't + // send pills in the plaintext body. contentText = this.plainWithPlainPills.serialize(editorState); contentHTML = mdWithPills.toHTML(); } @@ -1255,11 +1221,8 @@ export default class MessageComposerInput extends React.Component { // Move selection to the end of the selected history const change = editorState.change().collapseToEndOf(editorState.document); - // XXX: should we be calling this.onChange(change) now? - // Answer: yes, if we want it to do any of the fixups on stuff like emoji. - // however, this should already have been done and persisted in the history, - // so shouldn't be necessary. - + // We don't call this.onChange(change) now, as fixups on stuff like emoji + // should already have been done and persisted in the history. editorState = change.value; this.suppressAutoComplete = true; @@ -1362,6 +1325,8 @@ export default class MessageComposerInput extends React.Component { .insertText(suffix) .focus(); } + // for good hygiene, keep editorState updated to track the result of the change + // even though we don't do anything subsequently with it editorState = change.value; this.onChange(change, activeEditorState); @@ -1460,10 +1425,11 @@ export default class MessageComposerInput extends React.Component { }; onFormatButtonClicked = (name, e) => { - e.preventDefault(); // don't steal focus from the editor! + e.preventDefault(); // XXX: horrible evil hack to ensure the editor is focused so the act // of focusing it doesn't then cancel the format button being pressed + // FIXME: can we just tell handleKeyCommand's change to invoke .focus()? if (document.activeElement && document.activeElement.className !== 'mx_MessageComposer_editor') { this.refs.editor.focus(); setTimeout(()=>{ diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 708071df23..662fbc7104 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -10,7 +10,6 @@ const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput' import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import RoomMember from 'matrix-js-sdk'; -/* function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); if (components && components.length) { @@ -21,7 +20,9 @@ function addTextToDraft(text) { } } -describe('MessageComposerInput', () => { +// FIXME: These tests need to be updated from Draft to Slate. + +xdescribe('MessageComposerInput', () => { let parentDiv = null, sandbox = null, client = null, @@ -300,5 +301,4 @@ describe('MessageComposerInput', () => { expect(spy.args[0][1].body).toEqual('[Click here](https://some.lovely.url)'); expect(spy.args[0][1].formatted_body).toEqual('Click here'); }); -}); -*/ \ No newline at end of file +}); \ No newline at end of file From 0d0934add7d83d7b1eac6ca5f92c9b985eee58c9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 Jul 2018 00:58:35 +0100 Subject: [PATCH 11/24] unbreak modifier+space (e.g. emoji insert on macOS) (cherry picked from commit c490f87) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 77cc3ca2de..24834d01d7 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -713,6 +713,10 @@ export default class MessageComposerInput extends React.Component { }; onSpace = (ev: KeyboardEvent, change: Change): Change => { + if (ev.metaKey || ev.altKey || ev.shiftKey || ev.ctrlKey) { + return; + } + // drop a point in history so the user can undo a word // XXX: this seems nasty but adding to history manually seems a no-go ev.preventDefault(); From 8bcb987f507e485d84ec768d596a920d40fb6c6d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Jul 2018 20:14:37 +0100 Subject: [PATCH 12/24] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/CommandProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 0377ff3395..850d97ab71 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -43,7 +43,7 @@ export default class CommandProvider extends AutocompleteProvider { let matches = []; // check if the full match differs from the first word (i.e. returns false if the command has args) - if (command[0] !== command[1]) { + if (command[0] !== command[1]) { // The input looks like a command with arguments, perform exact match const name = command[1].substr(1); // strip leading `/` if (CommandMap[name]) { From 51591a4d62ac056d3d730a212e6cb2bf666b1ed7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 Jul 2018 20:11:17 +0100 Subject: [PATCH 13/24] fix lint --- src/components/structures/ContextualMenu.js | 3 ++- src/components/views/rooms/EventTile.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index adc8dfd11c..7295fd45d3 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -220,7 +220,8 @@ export default class ContextualMenu extends React.Component { { chevron } - { props.hasBackground && } + { props.hasBackground && } ; } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 9ed73c39b1..fff04d476d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -56,7 +56,7 @@ const stateEventTileTypes = { 'm.room.topic': 'messages.TextualEvent', 'm.room.power_levels': 'messages.TextualEvent', 'm.room.pinned_events': 'messages.TextualEvent', - 'm.room.server_acl' : 'messages.TextualEvent', + 'm.room.server_acl': 'messages.TextualEvent', 'im.vector.modular.widgets': 'messages.TextualEvent', }; From 58301e5dd455e4f992a1a4feb107f1224f155329 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Jul 2018 10:28:17 +0100 Subject: [PATCH 14/24] navigateHistory only when at edges of document, to prevent Firefox bug Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/MessageComposerInput.js | 39 ++++--------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 24834d01d7..0f0d8787d6 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1140,41 +1140,18 @@ export default class MessageComposerInput extends React.Component { // Select history only if we are not currently auto-completing if (this.autocomplete.state.completionList.length === 0) { + const selection = this.state.editorState.selection; - // 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(); + // selection must be collapsed + if (!selection.isCollapsed) return; + const document = this.state.editorState.document; - const editorNode = ReactDOM.findDOMNode(this.refs.editor); - const editorRect = editorNode.getBoundingClientRect(); - - // heuristic to handle tall emoji, pills, etc pushing the cursor away from the top - // or bottom of the page. - // XXX: is this going to break on large inline images or top-to-bottom scripts? - const EDGE_THRESHOLD = 15; - - let navigateHistory = false; + // and we must be at the edge of the document (up=start, down=end) if (up) { - const scrollCorrection = editorNode.scrollTop; - const distanceFromTop = cursorRect.top - editorRect.top + scrollCorrection; - console.log(`Cursor distance from editor top is ${distanceFromTop}`); - if (distanceFromTop < EDGE_THRESHOLD) { - navigateHistory = true; - } + if (!selection.isAtStartOf(document)) return; + } else { + if (!selection.isAtEndOf(document)) return; } - else { - const scrollCorrection = - editorNode.scrollHeight - editorNode.clientHeight - editorNode.scrollTop; - const distanceFromBottom = editorRect.bottom - cursorRect.bottom + scrollCorrection; - console.log(`Cursor distance from editor bottom is ${distanceFromBottom}`); - if (distanceFromBottom < EDGE_THRESHOLD) { - navigateHistory = true; - } - } - - if (!navigateHistory) return; const selected = this.selectHistory(up); if (selected) { From 100ecfe7cef57fda55d38e01d7c86160b09b608b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Jul 2018 10:29:52 +0100 Subject: [PATCH 15/24] remove trailing spaces to make linter happy (no-trailing-spaces) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 0f0d8787d6..fe2cf585ea 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -183,7 +183,7 @@ export default class MessageComposerInput extends React.Component { this.md = new Md({ rules: [ { - // if serialize returns undefined it falls through to the default hardcoded + // if serialize returns undefined it falls through to the default hardcoded // serialization rules serialize: (obj, children) => { if (obj.object !== 'inline') return; @@ -304,7 +304,7 @@ export default class MessageComposerInput extends React.Component { * - contentState was passed in */ createEditorState(richText: boolean, // eslint-disable-line no-unused-vars - editorState: ?Value): Value { + editorState: ?Value): Value { if (editorState instanceof Value) { return editorState; } @@ -359,7 +359,7 @@ export default class MessageComposerInput extends React.Component { // If so, what should be the format, and how do we differentiate it from replies? const quote = Block.create('block-quote'); - if (this.state.isRichTextEnabled) { + if (this.state.isRichTextEnabled) { let change = editorState.change(); if (editorState.anchorText.text === '' && editorState.anchorBlock.nodes.size === 1) { // replace the current block rather than split the block From abbb69dc3666997ec138a77677c052beb200be5e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Jul 2018 17:35:13 +0100 Subject: [PATCH 16/24] fix fn call, fixes usage of SlashCommands Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index fe2cf585ea..e7fbbae8a8 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -988,7 +988,7 @@ export default class MessageComposerInput extends React.Component { firstGrandChild.text[0] === '/') { commandText = this.plainWithIdPills.serialize(editorState); - cmd = SlashCommands.processInput(this.props.room.roomId, commandText); + cmd = processCommandInput(this.props.room.roomId, commandText); } if (cmd) { From fd4f9679df7b035c0112dce135c47967713f9518 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jul 2018 09:39:14 +0100 Subject: [PATCH 17/24] convert md<->rt if the stored editorState was in a different state Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/MessageComposerInput.js | 58 +++++++++++-------- src/stores/MessageComposerStore.js | 5 +- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index e7fbbae8a8..c8d8c9d2f9 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -143,27 +143,6 @@ export default class MessageComposerInput extends React.Component { Analytics.setRichtextMode(isRichTextEnabled); - this.state = { - // whether we're in rich text or markdown mode - isRichTextEnabled, - - // the currently displayed editor state (note: this is always what is modified on input) - editorState: this.createEditorState( - isRichTextEnabled, - MessageComposerStore.getEditorState(this.props.room.roomId), - ), - - // the original editor state, before we started tabbing through completions - originalEditorState: null, - - // the virtual state "above" the history stack, the message currently being composed that - // we want to persist whilst browsing history - currentlyComposedEditorState: null, - - // whether there were any completions - someCompletions: null, - }; - this.client = MatrixClientPeg.get(); // track whether we should be trying to show autocomplete suggestions on the current editor @@ -296,19 +275,47 @@ export default class MessageComposerInput extends React.Component { } ] }); + + const savedState = MessageComposerStore.getEditorState(this.props.room.roomId); + this.state = { + // whether we're in rich text or markdown mode + isRichTextEnabled, + + // the currently displayed editor state (note: this is always what is modified on input) + editorState: this.createEditorState( + isRichTextEnabled, + savedState.editor_state, + savedState.rich_text, + ), + + // the original editor state, before we started tabbing through completions + originalEditorState: null, + + // the virtual state "above" the history stack, the message currently being composed that + // we want to persist whilst browsing history + currentlyComposedEditorState: null, + + // whether there were any completions + someCompletions: null, + }; } /* * "Does the right thing" to create an Editor value, based on: * - whether we've got rich text mode enabled * - contentState was passed in + * - whether the contentState that was passed in was rich text */ - createEditorState(richText: boolean, // eslint-disable-line no-unused-vars - editorState: ?Value): Value { + createEditorState(wantRichText: boolean, editorState: ?Value, wasRichText: ?boolean): Value { if (editorState instanceof Value) { + if (wantRichText && !wasRichText) { + return this.mdToRichEditorState(editorState); + } + if (wasRichText && !wantRichText) { + return this.richToMdEditorState(editorState); + } return editorState; - } - else { + } else { // ...or create a new one. return Plain.deserialize('', { defaultBlock: DEFAULT_NODE }); } @@ -569,6 +576,7 @@ export default class MessageComposerInput extends React.Component { dis.dispatch({ action: 'editor_state', room_id: this.props.room.roomId, + rich_text: this.state.isRichTextEnabled, editor_state: editorState, }); diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js index 0e6c856e1b..e70714ff98 100644 --- a/src/stores/MessageComposerStore.js +++ b/src/stores/MessageComposerStore.js @@ -54,7 +54,10 @@ class MessageComposerStore extends Store { _editorState(payload) { const editorStateMap = this._state.editorStateMap; - editorStateMap[payload.room_id] = payload.editor_state; + editorStateMap[payload.room_id] = { + editor_state: payload.editor_state, + rich_text: payload.rich_text, + }; localStorage.setItem('editor_state', JSON.stringify(editorStateMap)); this._setState({ editorStateMap: editorStateMap, From c3aef6e3a0d153eb38eb5f1b8ac864896b753367 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jul 2018 10:29:14 +0100 Subject: [PATCH 18/24] workaround for tommoor/slate-md-serializer#14 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index c8d8c9d2f9..d0079309e3 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -182,6 +182,11 @@ export default class MessageComposerInput extends React.Component { return `${ children }`; case 'deleted': return ` ${ children }`; + case 'code': + // XXX: we only ever get given `code` regardless of whether it was inline or block + // XXX: workaround for https://github.com/tommoor/slate-md-serializer/issues/14 + // strip single backslashes from children, as they would have been escaped here + return `\`${ children.split('\\').map((v) => v ? v : '\\').join('') }\``; } }, }, From 95909de446f141a01e3eeca56906e8ce69f22b7e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jul 2018 11:39:55 +0100 Subject: [PATCH 19/24] fix MessageComposer not marking translatable strings. run gen-i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposer.js | 36 ++++++++++++------- src/i18n/strings/en_EN.json | 17 +++++---- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 746f3909f2..657fd463fd 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import CallHandler from '../../../CallHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; @@ -26,6 +26,17 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import Stickerpicker from './Stickerpicker'; +const formatButtonList = [ + _td("bold"), + _td("italic"), + _td("deleted"), + _td("underlined"), + _td("inline-code"), + _td("block-quote"), + _td("bulleted-list"), + _td("numbered-list"), +]; + export default class MessageComposer extends React.Component { constructor(props, context) { super(props, context); @@ -322,18 +333,17 @@ export default class MessageComposer extends React.Component { let formatBar; if (this.state.showFormatting && this.state.inputState.isRichTextEnabled) { const {marks, blockType} = this.state.inputState; - const formatButtons = ["bold", "italic", "deleted", "underlined", "inline-code", "block-quote", "bulleted-list", "numbered-list"].map( - (name) => { - const active = marks.some(mark => mark.type === name) || blockType === name; - const suffix = active ? '-on' : ''; - const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); - const className = 'mx_MessageComposer_format_button mx_filterFlipColor'; - return ; + const formatButtons = formatButtonList.map((name) => { + const active = marks.some(mark => mark.type === name) || blockType === name; + const suffix = active ? '-on' : ''; + const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); + const className = 'mx_MessageComposer_format_button mx_filterFlipColor'; + return ; }, ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 486ecdf114..2da820d751 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,6 +406,14 @@ "Invited": "Invited", "Filter room members": "Filter room members", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", + "bold": "bold", + "italic": "italic", + "deleted": "deleted", + "underlined": "underlined", + "inline-code": "inline-code", + "block-quote": "block-quote", + "bulleted-list": "bulleted-list", + "numbered-list": "numbered-list", "Attachment": "Attachment", "At this time it is not possible to reply with a file so this will be sent without being a reply.": "At this time it is not possible to reply with a file so this will be sent without being a reply.", "Upload Files": "Upload Files", @@ -430,14 +438,6 @@ "Command error": "Command error", "Unable to reply": "Unable to reply", "At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.", - "bold": "bold", - "italic": "italic", - "strike": "strike", - "underline": "underline", - "code": "code", - "quote": "quote", - "bullet": "bullet", - "numbullet": "numbullet", "Markdown is disabled": "Markdown is disabled", "Markdown is enabled": "Markdown is enabled", "No pinned messages.": "No pinned messages.", @@ -772,7 +772,6 @@ "Room directory": "Room directory", "Start chat": "Start chat", "And %(count)s more...|other": "And %(count)s more...", - "Share Link to User": "Share Link to User", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", "Matrix ID": "Matrix ID", From 3e05bf19c59a401efe53f39eed7be38aafb3b41f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jul 2018 16:30:45 +0100 Subject: [PATCH 20/24] hide autocomplete when moving caret to match existing behaviour Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/MessageComposerInput.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index d0079309e3..7b9fb89404 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -115,6 +115,15 @@ function onSendMessageFailed(err, room) { }); } +function rangeEquals(a: Range, b: Range): boolean { + return (a.anchorKey === b.anchorKey + && a.anchorOffset === b.anchorOffset + && a.focusKey === b.focusKey + && a.focusOffset === b.focusOffset + && a.isFocused === b.isFocused + && a.isBackward === b.isBackward); +} + /* * The textInput part of the MessageComposer */ @@ -469,8 +478,7 @@ export default class MessageComposerInput extends React.Component { } } - onChange = (change: Change, originalEditorState: value) => { - + onChange = (change: Change, originalEditorState?: Value) => { let editorState = change.value; if (this.direction !== '') { @@ -490,6 +498,11 @@ export default class MessageComposerInput extends React.Component { } } + // when selection changes hide the autocomplete + if (!rangeEquals(this.state.editorState.selection, editorState.selection)) { + this.autocomplete.hide(); + } + if (!editorState.document.isEmpty) { this.onTypingActivity(); } else { From b4bc09c3350dac7bdb867e0acb89e406ac6c9253 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Jul 2018 17:13:33 +0100 Subject: [PATCH 21/24] null-guard savedState since now we're accessing its props Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 7b9fb89404..f9395abe59 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -298,8 +298,8 @@ export default class MessageComposerInput extends React.Component { // the currently displayed editor state (note: this is always what is modified on input) editorState: this.createEditorState( isRichTextEnabled, - savedState.editor_state, - savedState.rich_text, + savedState ? savedState.editor_state : undefined, + savedState ? savedState.rich_text : undefined, ), // the original editor state, before we started tabbing through completions From 7405c5eff2f05d4a53633e936f515d70acc02924 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Jul 2018 16:35:42 +0100 Subject: [PATCH 22/24] specify alternate history storage key to prevent conflicts with draft Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index f9395abe59..89e4107a56 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -337,7 +337,7 @@ export default class MessageComposerInput extends React.Component { componentDidMount() { this.dispatcherRef = dis.register(this.onAction); - this.historyManager = new ComposerHistoryManager(this.props.room.roomId); + this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } componentWillUnmount() { From 59a14f2c0b562eda6c75747551df1673aa1aa15c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 15 Jul 2018 20:28:41 +0100 Subject: [PATCH 23/24] re-hydrate Values which have been serialized into LocalStorage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/stores/MessageComposerStore.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js index e70714ff98..1d37a7c9e5 100644 --- a/src/stores/MessageComposerStore.js +++ b/src/stores/MessageComposerStore.js @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd +Copyright 2017, 2018 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ limitations under the License. */ import dis from '../dispatcher'; import { Store } from 'flux/utils'; +import { Value } from 'slate'; const INITIAL_STATE = { // a map of room_id to rich text editor composer state @@ -65,7 +66,15 @@ class MessageComposerStore extends Store { } getEditorState(roomId) { - return this._state.editorStateMap[roomId]; + const editorStateMap = this._state.editorStateMap; + // const entry = this._state.editorStateMap[roomId]; + if (editorStateMap[roomId] && !Value.isValue(editorStateMap[roomId].editor_state)) { + // rehydrate lazily to prevent massive churn at launch and cache it + editorStateMap[roomId].editor_state = Value.fromJSON(editorStateMap[roomId].editor_state); + } + // explicitly don't setState here because the value didn't actually change, we just hydrated it, + // if a listener received an update they too would call this method and have a hydrated Value + return editorStateMap[roomId]; } reset() { From d7ff7cd4ed1e00c695b811b17e5313b1c3d8cf77 Mon Sep 17 00:00:00 2001 From: Matthew HodgsonDate: Mon, 16 Jul 2018 12:00:15 +0100 Subject: [PATCH 24/24] stupid thinkotypo --- docs/slate-formats.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/slate-formats.md b/docs/slate-formats.md index b526109c1b..7bb2fc9c5f 100644 --- a/docs/slate-formats.md +++ b/docs/slate-formats.md @@ -84,5 +84,5 @@ The actual conversion transitions are: * In RT mode, insert it straight into the editor as a fragment * In MD mode, serialise it to an MD string via slate-md-serializer and then insert the string into the editor as a fragment. -The various scenarios and transitions could be drawn into a pretty diagram if one felt the urge, but hopefully the enough +The various scenarios and transitions could be drawn into a pretty diagram if one felt the urge, but hopefully the above gives sufficient detail on how it's all meant to work. \ No newline at end of file