From c6837af398fcddfc8a409962ebb29ccb73ed5995 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 3 Jul 2018 21:36:31 +0100 Subject: [PATCH 01/24] import-type Change from slate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit 85ed499) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index eed6b18aa4..f5b22fd481 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -22,6 +22,7 @@ import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent'; import { Editor } from 'slate-react'; import { getEventTransfer } from 'slate-react'; import { Value, Document, Event, Block, Inline, Text, Range, Node } from 'slate'; +import type { Change } from 'slate'; import Html from 'slate-html-serializer'; import Md from 'slate-md-serializer'; From 372fa29ad31820b8a96c221bffb222634310cfd9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 3 Jul 2018 23:36:59 +0100 Subject: [PATCH 02/24] take edge into consideration when moving focus region on arrow keys fixes: >Pressing right when the caret is immediately left of some entity (pill, emojione emoji, etc..) causes the caret to jump to the left of the next entity (or end of the message if there are no more entities) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit 0982617) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index f5b22fd481..7ccee3ab16 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -449,13 +449,13 @@ export default class MessageComposerInput extends React.Component { if (this.direction !== '') { const focusedNode = editorState.focusInline || editorState.focusText; if (focusedNode.isVoid) { + const edge = this.direction === 'Previous' ? 'End' : 'Start'; if (editorState.isCollapsed) { - change = change[`collapseToEndOf${ this.direction }Text`](); - } - else { + change = change[`collapseTo${ edge }Of${ this.direction }Text`](); + } else { const block = this.direction === 'Previous' ? editorState.previousText : editorState.nextText; if (block) { - change = change.moveFocusToEndOf(block) + change = change[`moveFocusTo${ edge }Of`](block); } } editorState = change.value; From 483116fb0382d6e8710c9ea54e23e59e8c4ac232 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Jul 2018 00:04:56 +0100 Subject: [PATCH 03/24] add rule to slate-md-serializer: make underlined and removed work for CM Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit b521efd) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/MessageComposerInput.js | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 7ccee3ab16..6f2568d3b3 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -178,17 +178,27 @@ export default class MessageComposerInput extends React.Component { rules: [ { serialize: (obj, children) => { - if (obj.object === 'inline') { - switch (obj.type) { - case 'pill': - return `[${ obj.data.get('completion') }](${ obj.data.get('href') })`; - case 'emoji': - return obj.data.get('emojiUnicode'); - } + if (obj.object !== 'inline') return; + switch (obj.type) { + case 'pill': + return `[${ obj.data.get('completion') }](${ obj.data.get('href') })`; + case 'emoji': + return obj.data.get('emojiUnicode'); } - } - } - ] + }, + }, { + serialize: (obj, children) => { + if (obj.object !== 'mark') return; + // XXX: slate-md-serializer consumes marks other than bold, italic, code, inserted, deleted + switch (obj.type) { + case 'underlined': + return `${ children }`; + case 'deleted': + return `${ children }`; + } + }, + }, + ], }); this.html = new Html({ @@ -633,6 +643,7 @@ export default class MessageComposerInput extends React.Component { // FIXME: this conversion loses pills (turning them into pure MD links). // We need to add a pill-aware deserialize method // to PlainWithPillsSerializer which recognises pills in raw MD and turns them into pills. + debugger; return Plain.deserialize( // FIXME: we compile the MD out of the RTE state using slate-md-serializer // which doesn't roundtrip symmetrically with commonmark, which we use for @@ -688,7 +699,7 @@ export default class MessageComposerInput extends React.Component { return editorState.blocks.some(node => node.type == type) }; - onKeyDown = (ev: Event, change: Change, editor: Editor) => { + onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => { this.suppressAutoComplete = false; @@ -732,12 +743,21 @@ export default class MessageComposerInput extends React.Component { return this.onTab(ev); case KeyCode.ESCAPE: return this.onEscape(ev); + case KeyCode.SPACE: + return this.onSpace(ev, change); default: // don't intercept it return; } }; + onSpace = (ev: KeyboardEvent, change: Change): Change => { + // 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(); + return change.setOperationFlag("skip", false).setOperationFlag("merge", false).insertText(ev.key); + }; + onBackspace = (ev: Event, change: Change): Change => { if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { return; From faf17f06c6114cfd78e6573fb908654faac6cf42 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Jul 2018 00:15:36 +0100 Subject: [PATCH 04/24] remove debugger statement Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit b6f7940) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/MessageComposerInput.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 6f2568d3b3..67602e5f4a 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -643,7 +643,6 @@ export default class MessageComposerInput extends React.Component { // FIXME: this conversion loses pills (turning them into pure MD links). // We need to add a pill-aware deserialize method // to PlainWithPillsSerializer which recognises pills in raw MD and turns them into pills. - debugger; return Plain.deserialize( // FIXME: we compile the MD out of the RTE state using slate-md-serializer // which doesn't roundtrip symmetrically with commonmark, which we use for From 43204ea1772e258efd70b74e81b5fffd36ee4e52 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Jul 2018 10:17:05 +0100 Subject: [PATCH 05/24] fix Control-Backspace after select-all Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit 0f32ec0) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/MessageComposerInput.js | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 67602e5f4a..dc7cbf0b34 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -713,22 +713,6 @@ export default class MessageComposerInput extends React.Component { this.direction = ''; } - if (isOnlyCtrlOrCmdKeyEvent(ev)) { - const ctrlCmdCommand = { - // C-m => Toggles between rich text and markdown modes - [KeyCode.KEY_M]: 'toggle-mode', - [KeyCode.KEY_B]: 'bold', - [KeyCode.KEY_I]: 'italic', - [KeyCode.KEY_U]: 'underlined', - [KeyCode.KEY_J]: 'inline-code', - }[ev.keyCode]; - - if (ctrlCmdCommand) { - return this.handleKeyCommand(ctrlCmdCommand); - } - return; - } - switch (ev.keyCode) { case KeyCode.ENTER: return this.handleReturn(ev, change); @@ -744,9 +728,21 @@ export default class MessageComposerInput extends React.Component { return this.onEscape(ev); case KeyCode.SPACE: return this.onSpace(ev, change); - default: - // don't intercept it - return; + } + + if (isOnlyCtrlOrCmdKeyEvent(ev)) { + const ctrlCmdCommand = { + // C-m => Toggles between rich text and markdown modes + [KeyCode.KEY_M]: 'toggle-mode', + [KeyCode.KEY_B]: 'bold', + [KeyCode.KEY_I]: 'italic', + [KeyCode.KEY_U]: 'underlined', + [KeyCode.KEY_J]: 'inline-code', + }[ev.keyCode]; + + if (ctrlCmdCommand) { + return this.handleKeyCommand(ctrlCmdCommand); + } } }; @@ -757,15 +753,22 @@ export default class MessageComposerInput extends React.Component { return change.setOperationFlag("skip", false).setOperationFlag("merge", false).insertText(ev.key); }; - onBackspace = (ev: Event, change: Change): Change => { - if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { + onBackspace = (ev: KeyboardEvent, change: Change): Change => { + if (ev.metaKey || ev.altKey || ev.shiftKey) { return; } + const { editorState } = this.state; + + // Allow Ctrl/Cmd-Backspace when focus starts at the start of the composer (e.g select-all) + // for some reason if slate sees you Ctrl-backspace and your anchorOffset=0 it just resets your focus + if (!editorState.isCollapsed && editorState.anchorOffset === 0) { + return change.delete(); + } + if (this.state.isRichTextEnabled) { // let backspace exit lists const isList = this.hasBlock('list-item'); - const { editorState } = this.state; if (isList && editorState.anchorOffset == 0) { change From 5b74c615ae8c8cfb4693795695e76c5bde9e05cc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Jul 2018 10:54:38 +0100 Subject: [PATCH 06/24] add missing import Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit 47b6099) 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 dc7cbf0b34..3ef10ca79e 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -55,7 +55,7 @@ import Markdown from '../../../Markdown'; import ComposerHistoryManager from '../../../ComposerHistoryManager'; import MessageComposerStore from '../../../stores/MessageComposerStore'; -import {MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix'; +import {MATRIXTO_MD_LINK_PATTERN, MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g'); import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort, toShort} from 'emojione'; From 5bd4104c9650434b87cffbf2681793551dd62ea7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Jul 2018 12:16:11 +0100 Subject: [PATCH 07/24] modify ComposerHistoryManager Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit d139dd6) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/ComposerHistoryManager.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index e78fbcdc3b..0164e6c4cd 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -51,8 +51,8 @@ class HistoryItem { export default class ComposerHistoryManager { history: Array = []; prefix: string; - lastIndex: number = 0; - currentIndex: number = 0; + lastIndex: number = 0; // used for indexing the storage + currentIndex: number = 0; // used for indexing the loaded validated history Array constructor(roomId: string, prefix: string = 'mx_composer_history_') { this.prefix = prefix + roomId; @@ -69,18 +69,19 @@ export default class ComposerHistoryManager { } } this.lastIndex = this.currentIndex; + // reset currentIndex to account for any unserialisable history + this.currentIndex = this.history.length; } save(value: Value, format: MessageFormat) { const item = new HistoryItem(value, format); this.history.push(item); - this.currentIndex = this.lastIndex + 1; + this.currentIndex = this.history.length; sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); } getItem(offset: number): ?HistoryItem { - this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1); - const item = this.history[this.currentIndex]; - return item; + this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); + return this.history[this.currentIndex]; } } From 8665f10f275f5b735e180fa781de53af801149ee Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 8 Jul 2018 23:08:02 +0100 Subject: [PATCH 08/24] pin slate to 0.33.4 to avoid https://github.com/ianstormtaylor/slate/pull/1958 (cherry picked from commit 445faca) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d639a4e51c..661febf6af 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "react-beautiful-dnd": "^4.0.1", "react-dom": "^15.6.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", - "slate": "^0.33.4", + "slate": "0.33.4", "slate-react": "^0.12.4", "slate-html-serializer": "^0.6.1", "slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3", From 83f26149197b17ba3af77ec5b0263c16d6da6f3e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 Jul 2018 00:51:48 +0100 Subject: [PATCH 09/24] add guide to slate's data formats and how we convert (cherry picked from commit e7e4ee8) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- docs/slate-formats.md | 88 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/slate-formats.md diff --git a/docs/slate-formats.md b/docs/slate-formats.md new file mode 100644 index 0000000000..b526109c1b --- /dev/null +++ b/docs/slate-formats.md @@ -0,0 +1,88 @@ +Guide to data types used by the Slate-based Rich Text Editor +------------------------------------------------------------ + +We always store the Slate editor state in its Value form. + +The schema for the Value is the same whether the editor is in MD or rich text mode, and is currently (rather arbitrarily) +dictated by the schema expected by slate-md-serializer, simply because it was the only bit of the pipeline which +has opinions on the schema. (slate-html-serializer lets you define how to serialize whatever schema you like). + +The BLOCK_TAGS and MARK_TAGS give the mapping from HTML tags to the schema's node types (for blocks, which describe +block content like divs, and marks, which describe inline formatted sections like spans). + +We use

as the parent tag for the message (XXX: although some tags are technically not allowed to be nested within p's) + +Various conversions are performed as content is moved between HTML, MD, and plaintext representations of HTML and MD. + +The primitives used are: + + * Markdown.js - models commonmark-formatted MD strings (as entered by the composer in MD mode) + * toHtml() - renders them to HTML suitable for sending on the wire + * isPlainText() - checks whether the parsed MD contains anything other than simple text. + * toPlainText() - renders MD to plain text in order to remove backslashes. Only works if the MD is already plaintext (otherwise it just emits HTML) + + * slate-html-serializer + * converts Values to HTML (serialising) using our schema rules + * converts HTML to Values (deserialising) using our schema rules + + * slate-md-serializer + * converts rich Values to MD strings (serialising) but using a non-commonmark generic MD dialect. + * This should use commonmark, but we use the serializer here for expedience rather than writing a commonmark one. + + * slate-plain-serializer + * converts Values to plain text strings (serialising them) by concatenating the strings together + * converts Values from plain text strings (deserialiasing them). + * Used to initialise the editor by deserializing "" into a Value. Apparently this is the idiomatic way to initialise a blank editor. + * Used (as a bodge) to turn a rich text editor into a MD editor, when deserialising the converted MD string of the editor into a value + + * PlainWithPillsSerializer + * A fork of slate-plain-serializer which is aware of Pills (hence the name) and Emoji. + * It can be configured to output Pills as: + * "plain": Pills are rendered via their 'completion' text - e.g. 'Matthew'; used for sending messages) + * "md": Pills are rendered as MD, e.g. [Matthew](https://matrix.to/#/@matthew:matrix.org) ) + * "id": Pills are rendered as IDs, e.g. '@matthew:matrix.org' (used for authoring / commands) + * Emoji nodes are converted to inline utf8 emoji. + +The actual conversion transitions are: + + * Quoting: + * The message being quoted is taken as HTML + * ...and deserialised into a Value + * ...and then serialised into MD via slate-md-serializer if the editor is in MD mode + + * Roundtripping between MD and rich text editor mode + * From MD to richtext (mdToRichEditorState): + * Serialise the MD-format Value to a MD string (converting pills to MD) with PlainWithPillsSerializer in 'md' mode + * Convert that MD string to HTML via Markdown.js + * Deserialise that Value to HTML via slate-html-serializer + * From richtext to MD (richToMdEditorState): + * Serialise the richtext-format Value to a MD string with slate-md-serializer (XXX: this should use commonmark) + * Deserialise that to a plain text value via slate-plain-serializer + + * Loading history in one format into an editor which is in the other format + * Uses the same functions as for roundtripping + + * Scanning the editor for a slash command + * If the editor is a single line node starting with /, then serialize it to a string with PlainWithPillsSerializer in 'id' mode + So that pills get converted to IDs suitable for commands being passed around + + * Sending messages + * In RT mode: + * If there is rich content, serialize the RT-format Value to HTML body via slate-html-serializer + * Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode + * In MD mode: + * Serialize the MD-format Value into an MD string with PlainWithPillsSerializer in 'md' mode + * Parse the string with Markdown.js + * If it contains no formatting: + * Send as plaintext (as taken from Markdown.toPlainText()) + * Otherwise + * Send as HTML (as taken from Markdown.toHtml()) + * Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode + + * Pasting HTML + * Deserialize HTML to a RT Value via slate-html-serializer + * 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 +gives sufficient detail on how it's all meant to work. \ No newline at end of file From 021409aafe176d9783e5234f649e8f0414f6cd43 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 Jul 2018 00:52:27 +0100 Subject: [PATCH 10/24] apply review feedback from @lukebarnard1 (cherry picked from commit 37d4bce) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/HtmlUtils.js | 36 ------- src/Markdown.js | 8 -- src/autocomplete/Autocompleter.js | 6 +- src/autocomplete/UserProvider.js | 2 +- .../views/rooms/MessageComposerInput.js | 94 ++++++------------- .../views/rooms/MessageComposerInput-test.js | 8 +- 6 files changed, 38 insertions(+), 116 deletions(-) diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index ccfecb8081..09ce1187d5 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -112,42 +112,6 @@ export function charactersToImageNode(alt, useSvg, ...unicode) { />; } -/* -export function processHtmlForSending(html: string): string { - const contentDiv = document.createElement('div'); - contentDiv.innerHTML = html; - - if (contentDiv.children.length === 0) { - return contentDiv.innerHTML; - } - - let contentHTML = ""; - for (let i=0; i < contentDiv.children.length; i++) { - const element = contentDiv.children[i]; - if (element.tagName.toLowerCase() === 'p') { - contentHTML += element.innerHTML; - // Don't add a
for the last

- 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(/
\n/g, '\n').trim() + - '
'; - } 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: ( { 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 Hodgson Date: 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