commit
						eb497d442b
					
				|  | @ -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 <p/> 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 above | ||||
| gives sufficient detail on how it's all meant to work. | ||||
|  | @ -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", | ||||
|  |  | |||
|  | @ -51,8 +51,8 @@ class HistoryItem { | |||
| export default class ComposerHistoryManager { | ||||
|     history: Array<HistoryItem> = []; | ||||
|     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]; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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 <br /> for the last <p>
 | ||||
|             if (i !== contentDiv.children.length - 1) { | ||||
|                 contentHTML += '<br />'; | ||||
|             } | ||||
|         } else if (element.tagName.toLowerCase() === 'pre') { | ||||
|             // Replace "<br>\n" with "\n" within `<pre>` tags because the <br> 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 += '<pre>' + | ||||
|                 element.innerHTML.replace(/<br>\n/g, '\n').trim() + | ||||
|                 '</pre>'; | ||||
|         } 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. | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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 = { | ||||
|  |  | |||
|  | @ -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]) { | ||||
|  |  | |||
|  | @ -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: ( | ||||
|                         <PillCompletion | ||||
|  |  | |||
|  | @ -220,7 +220,8 @@ export default class ContextualMenu extends React.Component { | |||
|                 { chevron } | ||||
|                 <ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} /> | ||||
|             </div> | ||||
|             { props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> } | ||||
|             { props.hasBackground && <div className="mx_ContextualMenu_background" | ||||
|                                           onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> } | ||||
|             <style>{ chevronCSS }</style> | ||||
|         </div>; | ||||
|     } | ||||
|  |  | |||
|  | @ -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', | ||||
| }; | ||||
|  |  | |||
|  | @ -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 <img className={className} | ||||
|                                 title={_t(name)} | ||||
|                                 onMouseDown={onFormatButtonClicked} | ||||
|                                 key={name} | ||||
|                                 src={`img/button-text-${name}${suffix}.svg`} | ||||
|                                 height="17" />; | ||||
|             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 <img className={className} | ||||
|                             title={_t(name)} | ||||
|                             onMouseDown={onFormatButtonClicked} | ||||
|                             key={name} | ||||
|                             src={`img/button-text-${name}${suffix}.svg`} | ||||
|                             height="17" />; | ||||
|                 }, | ||||
|             ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,17 +21,14 @@ 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 { Value, Document, Block, Inline, Text, Range, Node } from 'slate'; | ||||
| import type { Change } from 'slate'; | ||||
| 
 | ||||
| import Html from 'slate-html-serializer'; | ||||
| import Md from 'slate-md-serializer'; | ||||
| import Plain from 'slate-plain-serializer'; | ||||
| import PlainWithPillsSerializer from "../../../autocomplete/PlainWithPillsSerializer"; | ||||
| 
 | ||||
| // import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
 | ||||
| //     getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState,
 | ||||
| //     Entity} from 'draft-js';
 | ||||
| 
 | ||||
| import classNames from 'classnames'; | ||||
| import Promise from 'bluebird'; | ||||
| 
 | ||||
|  | @ -54,7 +51,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'; | ||||
|  | @ -118,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 | ||||
|  */ | ||||
|  | @ -146,29 +152,18 @@ 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
 | ||||
|         // contents. currently it's only suppressed when navigating history to avoid ugly flashes
 | ||||
|         // of unexpected corrections as you navigate.
 | ||||
|         // XXX: should this be in state?
 | ||||
|         this.suppressAutoComplete = false; | ||||
| 
 | ||||
|         // track whether we've just pressed an arrowkey left or right in order to skip void nodes.
 | ||||
|         // see https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
 | ||||
|         this.direction = ''; | ||||
| 
 | ||||
|         this.plainWithMdPills    = new PlainWithPillsSerializer({ pillFormat: 'md' }); | ||||
|         this.plainWithIdPills    = new PlainWithPillsSerializer({ pillFormat: 'id' }); | ||||
|         this.plainWithPlainPills = new PlainWithPillsSerializer({ pillFormat: 'plain' }); | ||||
|  | @ -176,18 +171,35 @@ export default class MessageComposerInput extends React.Component { | |||
|         this.md = new Md({ | ||||
|             rules: [ | ||||
|                 { | ||||
|                     // if serialize returns undefined it falls through to the default hardcoded
 | ||||
|                     // serialization 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 `<u>${ children }</u>`; | ||||
|                             case 'deleted': | ||||
|                                 return `<del>${ children }</del>`; | ||||
|                             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('') }\``; | ||||
|                         } | ||||
|                     }, | ||||
|                 }, | ||||
|             ], | ||||
|         }); | ||||
| 
 | ||||
|         this.html = new Html({ | ||||
|  | @ -278,20 +290,46 @@ export default class MessageComposerInput extends React.Component { | |||
|             ] | ||||
|         }); | ||||
| 
 | ||||
|         this.suppressAutoComplete = false; | ||||
|         this.direction = ''; | ||||
|         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 ? savedState.editor_state : undefined, | ||||
|                 savedState ? savedState.rich_text : undefined, | ||||
|             ), | ||||
| 
 | ||||
|             // 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, 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 }); | ||||
|         } | ||||
|  | @ -299,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() { | ||||
|  | @ -342,7 +380,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
 | ||||
|  | @ -360,7 +398,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); | ||||
|  | @ -441,39 +478,37 @@ export default class MessageComposerInput extends React.Component { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onChange = (change: Change, originalEditorState: value) => { | ||||
| 
 | ||||
|     onChange = (change: Change, originalEditorState?: Value) => { | ||||
|         let editorState = change.value; | ||||
| 
 | ||||
|         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[`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; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // when selection changes hide the autocomplete
 | ||||
|         if (!rangeEquals(this.state.editorState.selection, editorState.selection)) { | ||||
|             this.autocomplete.hide(); | ||||
|         } | ||||
| 
 | ||||
|         if (!editorState.document.isEmpty) { | ||||
|             this.onTypingActivity(); | ||||
|         } else { | ||||
|             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; | ||||
|  | @ -501,9 +536,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) { | ||||
|  | @ -535,36 +568,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);
 | ||||
|  | @ -591,10 +594,10 @@ 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, | ||||
|         }); | ||||
| 
 | ||||
|         /* Since a modification was made, set originalEditorState to null, since newState is now our original */ | ||||
|         this.setState({ | ||||
|             editorState, | ||||
|             originalEditorState: originalEditorState || null | ||||
|  | @ -672,7 +675,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) | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|  | @ -684,10 +687,10 @@ 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: Event, change: Change, editor: Editor) => { | ||||
|     onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => { | ||||
| 
 | ||||
|         this.suppressAutoComplete = false; | ||||
| 
 | ||||
|  | @ -702,22 +705,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); | ||||
|  | @ -731,21 +718,53 @@ export default class MessageComposerInput extends React.Component { | |||
|                 return this.onTab(ev); | ||||
|             case KeyCode.ESCAPE: | ||||
|                 return this.onEscape(ev); | ||||
|             default: | ||||
|                 // don't intercept it
 | ||||
|                 return; | ||||
|             case KeyCode.SPACE: | ||||
|                 return this.onSpace(ev, change); | ||||
|         } | ||||
| 
 | ||||
|         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); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     onBackspace = (ev: Event, change: Change): Change => { | ||||
|         if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { | ||||
|     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(); | ||||
|         return change.setOperationFlag("skip", false).setOperationFlag("merge", false).insertText(ev.key); | ||||
|     }; | ||||
| 
 | ||||
|     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 | ||||
|  | @ -805,7 +824,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) { | ||||
|  | @ -816,7 +835,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 { | ||||
|  | @ -986,7 +1005,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); | ||||
|  | @ -995,7 +1014,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) { | ||||
|  | @ -1067,8 +1086,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(); | ||||
|             } | ||||
|  | @ -1147,41 +1166,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) { | ||||
|  | @ -1232,11 +1228,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; | ||||
|  | @ -1339,6 +1332,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); | ||||
|  | @ -1437,10 +1432,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(()=>{ | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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
 | ||||
|  | @ -54,7 +55,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, | ||||
|  | @ -62,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() { | ||||
|  |  | |||
|  | @ -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('<a href="https://some.lovely.url">Click here</a>'); | ||||
|     }); | ||||
| }); | ||||
| */ | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	 David Baker
						David Baker