diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 7d998f8c4b..c3bb34ae26 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -33,7 +33,6 @@ src/components/views/rooms/RoomList.js src/components/views/rooms/RoomPreviewBar.js src/components/views/rooms/SearchBar.js src/components/views/rooms/SearchResultTile.js -src/components/views/rooms/SlateMessageComposer.js src/components/views/settings/ChangeAvatar.js src/components/views/settings/ChangePassword.js src/components/views/settings/DevicesPanel.js diff --git a/package.json b/package.json index a1ebc6602d..ad446e26cc 100644 --- a/package.json +++ b/package.json @@ -103,10 +103,6 @@ "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", - "slate": "^0.41.2", - "slate-html-serializer": "^0.6.1", - "slate-md-serializer": "github:matrix-org/slate-md-serializer#f7c4ad3", - "slate-react": "^0.18.10", "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-animate": "^1.5.2", diff --git a/src/SlateComposerHistoryManager.js b/src/SlateComposerHistoryManager.js deleted file mode 100644 index 948dcf64ff..0000000000 --- a/src/SlateComposerHistoryManager.js +++ /dev/null @@ -1,86 +0,0 @@ -//@flow -/* -Copyright 2017 Aviral Dasgupta - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {Value} from 'slate'; - -import _clamp from 'lodash/clamp'; - -type MessageFormat = 'rich' | 'markdown'; - -class HistoryItem { - // We store history items in their native format to ensure history is accurate - // and then convert them if our RTE has subsequently changed format. - value: Value; - format: MessageFormat = 'rich'; - - constructor(value: ?Value, format: ?MessageFormat) { - this.value = value; - this.format = format; - } - - static fromJSON(obj: Object): HistoryItem { - return new HistoryItem( - Value.fromJSON(obj.value), - obj.format, - ); - } - - toJSON(): Object { - return { - value: this.value.toJSON(), - format: this.format, - }; - } -} - -export default class SlateComposerHistoryManager { - history: Array = []; - prefix: string; - 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; - - // TODO: Performance issues? - let item; - for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { - try { - this.history.push( - HistoryItem.fromJSON(JSON.parse(item)), - ); - } catch (e) { - console.warn("Throwing away unserialisable history", e); - } - } - 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.history.length; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); - } - - getItem(offset: number): ?HistoryItem { - this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); - return this.history[this.currentIndex]; - } -} diff --git a/src/autocomplete/PlainWithPillsSerializer.js b/src/autocomplete/PlainWithPillsSerializer.js deleted file mode 100644 index 09bb3772ac..0000000000 --- a/src/autocomplete/PlainWithPillsSerializer.js +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Based originally on slate-plain-serializer - -import { Block } from 'slate'; - -/** - * Plain text serializer, which converts a Slate `value` to a plain text string, - * serializing pills into various different formats as required. - * - * @type {PlainWithPillsSerializer} - */ - -class PlainWithPillsSerializer { - /* - * @param {String} options.pillFormat - either 'md', 'plain', 'id' - */ - constructor(options = {}) { - const { - pillFormat = 'plain', - } = options; - this.pillFormat = pillFormat; - } - - /** - * Serialize a Slate `value` to a plain text string, - * serializing pills as either MD links, plain text representations or - * ID representations as required. - * - * @param {Value} value - * @return {String} - */ - serialize = value => { - return this._serializeNode(value.document); - } - - /** - * Serialize a `node` to plain text. - * - * @param {Node} node - * @return {String} - */ - _serializeNode = node => { - if ( - node.object == 'document' || - (node.object == 'block' && Block.isBlockList(node.nodes)) - ) { - return node.nodes.map(this._serializeNode).join('\n'); - } else if (node.type == 'emoji') { - return node.data.get('emojiUnicode'); - } else if (node.type == 'pill') { - const completion = node.data.get('completion'); - // over the wire the @room pill is just plaintext - if (completion === '@room') return completion; - - switch (this.pillFormat) { - case 'plain': - return completion; - case 'md': - return `[${ completion }](${ node.data.get('href') })`; - case 'id': - return node.data.get('completionId') || completion; - } - } else if (node.nodes) { - return node.nodes.map(this._serializeNode).join(''); - } else { - return node.text; - } - } -} - -/** - * Export. - * - * @type {PlainWithPillsSerializer} - */ - -export default PlainWithPillsSerializer; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 7261af3bf0..3420e69ce6 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -393,13 +393,6 @@ const LoggedInView = createReactClass({ return; } - // XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036 - // If using Slate, consume the Backspace without first focusing as it causes an implosion - if (ev.key === Key.BACKSPACE && !SettingsStore.getValue("useCiderComposer")) { - ev.stopPropagation(); - return; - } - if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) { // synchronous dispatch so we focus before key generates input dis.dispatch({action: 'focus_composer'}, true); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 939f422a36..c552e2f8f5 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -157,8 +157,6 @@ module.exports = createReactClass({ canReact: false, canReply: false, - - useCider: false, }; }, @@ -180,18 +178,10 @@ module.exports = createReactClass({ WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate); - this._onCiderUpdated(); - this._ciderWatcherRef = SettingsStore.watchSetting( - "useCiderComposer", null, this._onCiderUpdated); - this._roomView = createRef(); this._searchResultsPanel = createRef(); }, - _onCiderUpdated: function() { - this.setState({useCider: SettingsStore.getValue("useCiderComposer")}); - }, - _onRoomViewStoreUpdate: function(initial) { if (this.unmounted) { return; @@ -1806,29 +1796,16 @@ module.exports = createReactClass({ myMembership === 'join' && !this.state.searchResults ); if (canSpeak) { - if (this.state.useCider) { - const MessageComposer = sdk.getComponent('rooms.MessageComposer'); - messageComposer = - ; - } else { - const SlateMessageComposer = sdk.getComponent('rooms.SlateMessageComposer'); - messageComposer = - ; - } + const MessageComposer = sdk.getComponent('rooms.MessageComposer'); + messageComposer = + ; } // TODO: Why aren't we storing the term/scope/count in this format diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js deleted file mode 100644 index 595ec0681f..0000000000 --- a/src/components/views/rooms/MessageComposerInput.js +++ /dev/null @@ -1,1516 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { Editor } from 'slate-react'; -import { getEventTransfer } from 'slate-react'; -import { Value, Block, Inline, Range } 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 classNames from 'classnames'; - -import MatrixClientPeg from '../../../MatrixClientPeg'; -import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; -import {processCommandInput} from '../../../SlashCommands'; -import { isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard'; -import Modal from '../../../Modal'; -import sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import Analytics from '../../../Analytics'; - -import dis from '../../../dispatcher'; - -import * as HtmlUtils from '../../../HtmlUtils'; -import Autocomplete from './Autocomplete'; -import {Completion} from "../../../autocomplete/Autocompleter"; -import Markdown from '../../../Markdown'; -import MessageComposerStore from '../../../stores/MessageComposerStore'; -import ContentMessages from '../../../ContentMessages'; - -import EMOTICON_REGEX from 'emojibase-regex/emoticon'; - -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; -import {getPrimaryPermalinkEntity, makeUserPermalink} from "../../../utils/permalinks/Permalinks"; -import ReplyPreview from "./ReplyPreview"; -import RoomViewStore from '../../../stores/RoomViewStore'; -import ReplyThread from "../elements/ReplyThread"; -import {ContentHelpers} from 'matrix-js-sdk'; -import AccessibleButton from '../elements/AccessibleButton'; -import {findEditableEvent} from '../../../utils/EventUtils'; -import SlateComposerHistoryManager from "../../../SlateComposerHistoryManager"; -import TypingStore from "../../../stores/TypingStore"; -import {EMOTICON_TO_EMOJI} from "../../../emoji"; - -const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); - -// the Slate node type to default to for unstyled text -const DEFAULT_NODE = 'paragraph'; - -// map HTML elements through to our Slate schema node types -// used for the HTML deserializer. -// (The names here are chosen to match the MD serializer's schema for convenience) -const BLOCK_TAGS = { - p: 'paragraph', - blockquote: 'block-quote', - ul: 'bulleted-list', - h1: 'heading1', - h2: 'heading2', - h3: 'heading3', - h4: 'heading4', - h5: 'heading5', - h6: 'heading6', - li: 'list-item', - ol: 'numbered-list', - pre: 'code', -}; - -const MARK_TAGS = { - strong: 'bold', - b: 'bold', // deprecated - em: 'italic', - i: 'italic', // deprecated - code: 'code', - u: 'underlined', - del: 'deleted', - strike: 'deleted', // deprecated - s: 'deleted', // deprecated -}; - -const SLATE_SCHEMA = { - inlines: { - pill: { - isVoid: true, - }, - emoji: { - isVoid: true, - }, - }, -}; - -function onSendMessageFailed(err, room) { - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('MessageComposer got send failure: ' + err.name + '('+err+')'); - dis.dispatch({ - action: 'message_send_failed', - }); -} - -function rangeEquals(a: Range, b: Range): boolean { - return (a.anchor.key === b.anchor.key - && a.anchor.offset === b.anchorOffset - && a.focus.key === b.focusKey - && a.focus.offset === b.focusOffset - && a.isFocused === b.isFocused - && a.isBackward === b.isBackward); -} - -/* - * The textInput part of the MessageComposer - */ -export default class MessageComposerInput extends React.Component { - static propTypes = { - // js-sdk Room object - room: PropTypes.object.isRequired, - - onInputStateChanged: PropTypes.func, - }; - - client: MatrixClient; - autocomplete: Autocomplete; - historyManager: SlateComposerHistoryManager; - - constructor(props) { - super(props); - - const isRichTextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'); - - Analytics.setRichtextMode(isRichTextEnabled); - - 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' }); - - 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') 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 }`; - 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({ - rules: [ - { - deserialize: (el, next) => { - const tag = el.tagName.toLowerCase(); - let type = BLOCK_TAGS[tag]; - if (type) { - return { - object: 'block', - type: type, - nodes: next(el.childNodes), - }; - } - type = MARK_TAGS[tag]; - if (type) { - return { - object: 'mark', - type: type, - nodes: next(el.childNodes), - }; - } - // special case links - if (tag === 'a') { - const href = el.getAttribute('href'); - const permalinkEntity = getPrimaryPermalinkEntity(href); - if (permalinkEntity) { - return { - object: 'inline', - type: 'pill', - data: { - href, - completion: el.innerText, - completionId: permalinkEntity, - }, - }; - } else { - return { - object: 'inline', - type: 'link', - data: { href }, - nodes: next(el.childNodes), - }; - } - } - }, - serialize: (obj, children) => { - if (obj.object === 'block') { - return this.renderNode({ - node: obj, - children: children, - }); - } else if (obj.object === 'mark') { - return this.renderMark({ - mark: obj, - children: children, - }); - } else if (obj.object === 'inline') { - // special case links, pills and emoji otherwise we - // end up with React components getting rendered out(!) - switch (obj.type) { - case 'pill': - return { obj.data.get('completion') }; - case 'link': - return { children }; - case 'emoji': - // XXX: apparently you can't return plain strings from serializer rules - // until https://github.com/ianstormtaylor/slate/pull/1854 is merged. - // So instead we temporarily wrap emoji from RTE in a span. - return { obj.data.get('emojiUnicode') }; - } - return this.renderNode({ - node: obj, - children: children, - }); - } - }, - }, - ], - }); - - 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(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 { - // ...or create a new one. and explicitly focus it otherwise tab in-out issues - const base = Plain.deserialize('', { defaultBlock: DEFAULT_NODE }); - return base.change().focus().value; - } - } - - componentWillMount() { - this.dispatcherRef = dis.register(this.onAction); - this.historyManager = new SlateComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); - } - - componentWillUnmount() { - dis.unregister(this.dispatcherRef); - } - - _collectEditor = (e) => { - this._editor = e; - } - - onAction = (payload) => { - const editorState = this.state.editorState; - - switch (payload.action) { - case 'reply_to_event': - case 'focus_composer': - this.focusComposer(); - break; - case 'insert_mention': - { - // Pretend that we've autocompleted this user because keeping two code - // paths for inserting a user pill is not fun - const selection = this.getSelectionRange(this.state.editorState); - const member = this.props.room.getMember(payload.user_id); - const completion = member ? - member.rawDisplayName : payload.user_id; - this.setDisplayedCompletion({ - completion, - completionId: payload.user_id, - selection, - href: makeUserPermalink(payload.user_id), - suffix: (selection.beginning && selection.start === 0) ? ': ' : ' ', - }); - } - break; - case 'quote': { - const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, { - forComposerQuote: true, - returnString: true, - }); - const fragment = this.html.deserialize(html); - // FIXME: do we want to put in a permalink to the original quote here? - // 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) { - let change = editorState.change(); - const anchorText = editorState.anchorText; - if ((!anchorText || anchorText.text === '') && editorState.anchorBlock.nodes.size === 1) { - // replace the current block rather than split the block - // XXX: this destroys our focus by deleting the thing we are anchored/focused on - change = change.replaceNodeByKey(editorState.anchorBlock.key, quote); - } else { - // insert it into the middle of the block (splitting it) - change = change.insertBlock(quote); - } - - // XXX: heuristic to strip out wrapping

which breaks quoting in RT mode - if (fragment.document.nodes.size && fragment.document.nodes.get(0).type === DEFAULT_NODE) { - change = change.insertFragmentByKey(quote.key, 0, fragment.document.nodes.get(0)); - } else { - change = change.insertFragmentByKey(quote.key, 0, fragment.document); - } - - // XXX: this is to bring back the focus in a sane place and add a paragraph after it - change = change.select(Range.create({ - anchor: { - key: quote.key, - }, - focus: { - key: quote.key, - }, - })).moveToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus(); - - this.onChange(change); - } else { - const fragmentChange = fragment.change(); - fragmentChange.moveToRangeOfNode(fragment.document) - .wrapBlock(quote); - - // FIXME: handle pills and use commonmark rather than md-serialize - const md = this.md.serialize(fragmentChange.value); - const change = editorState.change() - .insertText(md + '\n\n') - .focus(); - this.onChange(change); - } - } - break; - } - }; - - onChange = (change: Change, originalEditorState?: Value) => { - let editorState = change.value; - - if (this.direction !== '') { - const focusedNode = editorState.focusInline || editorState.focusText; - if (editorState.schema.isVoid(focusedNode)) { - // XXX: does this work in RTL? - const edge = this.direction === 'Previous' ? 'End' : 'Start'; - if (editorState.selection.isCollapsed) { - change = change[`moveTo${ edge }Of${ this.direction }Text`](); - } else { - const block = this.direction === 'Previous' ? editorState.previousText : editorState.nextText; - if (block) { - change = change[`moveFocusTo${ edge }OfNode`](block); - } - } - editorState = change.value; - } - } - - // when in autocomplete mode and selection changes hide the autocomplete. - // Selection changes when we enter text so use a heuristic to compare documents without doing it recursively - if (this.autocomplete.state.completionList.length > 0 && !this.autocomplete.state.hide && - !rangeEquals(this.state.editorState.selection, editorState.selection) && - // XXX: the heuristic failed when inlines like pills weren't taken into account. This is inideal - this.state.editorState.document.toJSON() === editorState.document.toJSON()) { - this.autocomplete.hide(); - } - - if (Plain.serialize(editorState) !== '') { - TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, true); - } else { - TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, false); - } - - if (editorState.startText !== null) { - const text = editorState.startText.text; - const currentStartOffset = editorState.selection.start.offset; - - // Automatic replacement of plaintext emoji to Unicode emoji - if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { - // The first matched group includes just the matched plaintext emoji - const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset)); - if (emoticonMatch) { - const query = emoticonMatch[1].toLowerCase().replace("-", ""); - const data = EMOTICON_TO_EMOJI.get(query); - - // only perform replacement if we found a match, otherwise we would be not letting user type - if (data) { - const range = Range.create({ - anchor: { - key: editorState.startText.key, - offset: currentStartOffset - emoticonMatch[1].length - 1, - }, - focus: { - key: editorState.startText.key, - offset: currentStartOffset - 1, - }, - }); - change = change.insertTextAtRange(range, data.unicode); - editorState = change.value; - } - } - } - } - - 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); - - if (blockType === 'list-item') { - const parent = editorState.document.getParent(editorState.blocks.first().key); - if (parent.type === 'numbered-list') { - blockType = 'numbered-list'; - } else if (parent.type === 'bulleted-list') { - blockType = 'bulleted-list'; - } - } - const inputState = { - marks: editorState.activeMarks, - blockType, - }; - this.props.onInputStateChanged(inputState); - } - - // Record the editor state for this room so that it can be retrieved after switching to another room and back - MessageComposerStore.setEditorState(this.props.room.roomId, editorState, this.state.isRichTextEnabled); - - this.setState({ - editorState, - originalEditorState: originalEditorState || null, - }); - }; - - mdToRichEditorState(editorState: Value): Value { - // for consistency when roundtripping, we could use slate-md-serializer rather than - // commonmark, but then we would lose pills as the MD deserialiser doesn't know about - // them and doesn't have any extensibility hooks. - // - // The code looks like this: - // - // const markdown = this.plainWithMdPills.serialize(editorState); - // - // // weirdly, the Md serializer can't deserialize '' to a valid Value... - // if (markdown !== '') { - // editorState = this.md.deserialize(markdown); - // } - // else { - // editorState = Plain.deserialize('', { defaultBlock: DEFAULT_NODE }); - // } - - // so, instead, we use commonmark proper (which is arguably more logical to the user - // anyway, as they'll expect the RTE view to match what they'll see in the timeline, - // but the HTML->MD conversion is anyone's guess). - - const textWithMdPills = this.plainWithMdPills.serialize(editorState); - const markdown = new Markdown(textWithMdPills); - // HTML deserialize has custom rules to turn permalinks into pill objects. - return this.html.deserialize(markdown.toHTML()); - } - - richToMdEditorState(editorState: Value): Value { - // 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. - 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 - // compiling MD out of the MD editor state above. - this.md.serialize(editorState), - { defaultBlock: DEFAULT_NODE }, - ); - } - - enableRichtext(enabled: boolean) { - if (enabled === this.state.isRichTextEnabled) return; - - Analytics.setRichtextMode(enabled); - - this.setState({ - editorState: this.createEditorState( - enabled, - this.state.editorState, - this.state.isRichTextEnabled, - ), - isRichTextEnabled: enabled, - }, () => { - this._editor.focus(); - if (this.props.onInputStateChanged) { - this.props.onInputStateChanged({ - isRichTextEnabled: enabled, - }); - } - }); - - SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled); - } - - /** - * Check if the current selection has a mark with `type` in it. - * - * @param {String} type - * @return {Boolean} - */ - - hasMark = type => { - const { editorState } = this.state; - return editorState.activeMarks.some(mark => mark.type === type); - }; - - /** - * Check if the any of the currently selected blocks are of `type`. - * - * @param {String} type - * @return {Boolean} - */ - - hasBlock = type => { - const { editorState } = this.state; - return editorState.blocks.some(node => node.type === type); - }; - - onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => { - this.suppressAutoComplete = false; - this.direction = ''; - - // Navigate autocomplete list with arrow keys - if (this.autocomplete.countCompletions() > 0) { - if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) { - switch (ev.key) { - case Key.ARROW_UP: - this.autocomplete.moveSelection(-1); - ev.preventDefault(); - return true; - case Key.ARROW_DOWN: - this.autocomplete.moveSelection(+1); - ev.preventDefault(); - return true; - } - } - } - - // skip void nodes - see - // https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095 - if (ev.key === Key.ARROW_LEFT) { - this.direction = 'Previous'; - } else if (ev.key === Key.ARROW_RIGHT) { - this.direction = 'Next'; - } - - switch (ev.key) { - case Key.ENTER: - return this.handleReturn(ev, change); - case Key.BACKSPACE: - return this.onBackspace(ev, change); - case Key.ARROW_UP: - return this.onVerticalArrow(ev, true); - case Key.ARROW_DOWN: - return this.onVerticalArrow(ev, false); - case Key.TAB: - return this.onTab(ev); - case Key.ESCAPE: - return this.onEscape(ev); - case Key.SPACE: - return this.onSpace(ev, change); - } - - if (isOnlyCtrlOrCmdKeyEvent(ev)) { - const ctrlCmdCommand = { - // C-m => Toggles between rich text and markdown modes - [Key.M]: 'toggle-mode', - [Key.B]: 'bold', - [Key.I]: 'italic', - [Key.U]: 'underlined', - [Key.J]: 'inline-code', - }[ev.key]; - - if (ctrlCmdCommand) { - ev.preventDefault(); // to prevent clashing with Mac's minimize window - return this.handleKeyCommand(ctrlCmdCommand); - } - } - }; - - 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.withoutMerging(() => { - change.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 anchor.offset=0 it just resets your focus - // XXX: Doing this now seems to put slate into a broken state, and it didn't appear to be doing - // what it claims to do on the old version of slate anyway... - /*if (!editorState.isCollapsed && editorState.selection.anchor.offset === 0) { - return change.delete(); - }*/ - - if (this.state.isRichTextEnabled) { - // let backspace exit lists - const isList = this.hasBlock('list-item'); - - if (isList && editorState.selection.anchor.offset == 0) { - change - .setBlocks(DEFAULT_NODE) - .unwrapBlock('bulleted-list') - .unwrapBlock('numbered-list'); - return change; - } else if (editorState.selection.anchor.offset == 0 && editorState.isCollapsed) { - // turn blocks back into paragraphs - if ((this.hasBlock('block-quote') || - this.hasBlock('heading1') || - this.hasBlock('heading2') || - this.hasBlock('heading3') || - this.hasBlock('heading4') || - this.hasBlock('heading5') || - this.hasBlock('heading6') || - this.hasBlock('code'))) { - return change.setBlocks(DEFAULT_NODE); - } - - // remove paragraphs entirely if they're nested - const parent = editorState.document.getParent(editorState.anchorBlock.key); - if (editorState.selection.anchor.offset == 0 && - this.hasBlock('paragraph') && - parent.nodes.size == 1 && - parent.object !== 'document') { - return change.replaceNodeByKey(editorState.anchorBlock.key, editorState.anchorText) - .moveToEndOfNode(parent) - .focus(); - } - } - } - return; - }; - - handleKeyCommand = (command: string): boolean => { - if (command === 'toggle-mode') { - this.enableRichtext(!this.state.isRichTextEnabled); - return true; - } - - //const newState: ?Value = null; - - // Draft handles rich text mode commands by default but we need to do it ourselves for Markdown. - if (this.state.isRichTextEnabled) { - const type = command; - const { editorState } = this.state; - const change = editorState.change(); - const { document } = editorState; - switch (type) { - // list-blocks: - case 'bulleted-list': - case 'numbered-list': { - // 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); - }); - - if (isList && isType) { - change - .setBlocks(DEFAULT_NODE) - .unwrapBlock('bulleted-list') - .unwrapBlock('numbered-list'); - } else if (isList) { - change - .unwrapBlock( - type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list', - ) - .wrapBlock(type); - } else { - change.setBlocks('list-item').wrapBlock(type); - } - } - break; - - // simple blocks - case 'paragraph': - case 'block-quote': - case 'heading1': - case 'heading2': - case 'heading3': - case 'heading4': - case 'heading5': - case 'heading6': - case 'list-item': - case 'code': { - const isActive = this.hasBlock(type); - const isList = this.hasBlock('list-item'); - - if (isList) { - change - .setBlocks(isActive ? DEFAULT_NODE : type) - .unwrapBlock('bulleted-list') - .unwrapBlock('numbered-list'); - } else { - change.setBlocks(isActive ? DEFAULT_NODE : type); - } - } - break; - - // marks: - case 'bold': - case 'italic': - case 'inline-code': - case 'underlined': - case 'deleted': { - change.toggleMark(type === 'inline-code' ? 'code' : type); - } - break; - - default: - console.warn(`ignoring unrecognised RTE command ${type}`); - return false; - } - - this.onChange(change); - - return true; - } else { -/* - const contentState = this.state.editorState.getCurrentContent(); - const multipleLinesSelected = RichText.hasMultiLineSelection(this.state.editorState); - - const selectionState = this.state.editorState.getSelection(); - const start = selectionState.getStartOffset(); - const end = selectionState.getEndOffset(); - - // If multiple lines are selected or nothing is selected, insert a code block - // instead of applying inline code formatting. This is an attempt to mimic what - // happens in non-MD mode. - const treatInlineCodeAsBlock = multipleLinesSelected || start === end; - const textMdCodeBlock = (text) => `\`\`\`\n${text}\n\`\`\`\n`; - const modifyFn = { - 'bold': (text) => `**${text}**`, - 'italic': (text) => `*${text}*`, - 'underline': (text) => `${text}`, - 'strike': (text) => `${text}`, - // ("code" is triggered by ctrl+j by draft-js by default) - 'code': (text) => treatInlineCodeAsBlock ? textMdCodeBlock(text) : `\`${text}\``, - 'code': textMdCodeBlock, - 'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join('') + '\n', - 'unordered-list-item': (text) => text.split('\n').map((line) => `\n- ${line}`).join(''), - 'ordered-list-item': (text) => text.split('\n').map((line, i) => `\n${i + 1}. ${line}`).join(''), - }[command]; - - const selectionAfterOffset = { - 'bold': -2, - 'italic': -1, - 'underline': -4, - 'strike': -6, - 'code': treatInlineCodeAsBlock ? -5 : -1, - 'code': -5, - 'blockquote': -2, - }[command]; - - // Returns a function that collapses a selection to its end and moves it by offset - const collapseAndOffsetSelection = (selection, offset) => { - const key = selection.endKey(); - return new Range({ - anchorKey: key, anchor.offset: offset, - focus.key: key, focus.offset: offset, - }); - }; - - if (modifyFn) { - - const previousSelection = this.state.editorState.getSelection(); - const newContentState = RichText.modifyText(contentState, previousSelection, modifyFn); - newState = EditorState.push( - this.state.editorState, - newContentState, - 'insert-characters', - ); - - let newSelection = newContentState.getSelectionAfter(); - // If the selection range is 0, move the cursor inside the formatted body - if (previousSelection.getStartOffset() === previousSelection.getEndOffset() && - previousSelection.getStartKey() === previousSelection.getEndKey() && - selectionAfterOffset !== undefined - ) { - const selectedBlock = newContentState.getBlockForKey(previousSelection.getAnchorKey()); - const blockLength = selectedBlock.getText().length; - const newOffset = blockLength + selectionAfterOffset; - newSelection = collapseAndOffsetSelection(newSelection, newOffset); - } - - newState = EditorState.forceSelection(newState, newSelection); - } - } - - if (newState != null) { - this.setState({editorState: newState}); - return true; - } -*/ - } - return false; - }; - - onPaste = (event: Event, change: Change, editor: Editor): Change => { - const transfer = getEventTransfer(event); - - switch (transfer.type) { - case 'files': - // This actually not so much for 'files' as such (at time of writing - // neither chrome nor firefox let you paste a plain file copied - // from Finder) but more images copied from a different website - // / word processor etc. - return ContentMessages.sharedInstance().sendContentListToRoom( - transfer.files, this.props.room.roomId, this.client, - ); - case 'html': { - if (this.state.isRichTextEnabled) { - // FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means - // that we will silently discard nested blocks (e.g. nested lists) :( - const fragment = this.html.deserialize(transfer.html); - return change - // XXX: this somehow makes Slate barf on undo and get too empty and break entirely - // .setOperationFlag("skip", false) - // .setOperationFlag("merge", false) - .insertFragment(fragment.document); - } else { - // in MD mode we don't want the rich content pasted as the magic was annoying people so paste plain - return change.withoutMerging(() => { - change.insertText(transfer.text); - }); - } - } - case 'text': - // don't skip/merge so that multiple consecutive pastes can be undone individually - return change.withoutMerging(() => { - change.insertText(transfer.text); - }); - } - }; - - handleReturn = (ev, change) => { - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; - if (ev.shiftKey || (isMac && ev.altKey)) { - return change.insertText('\n'); - } - - if (this.autocomplete.hasSelection()) { - this.autocomplete.hide(); - ev.preventDefault(); - return true; - } - - const editorState = this.state.editorState; - - const lastBlock = editorState.blocks.last(); - if (['code', 'block-quote', 'list-item'].includes(lastBlock.type)) { - const text = lastBlock.text; - if (text === '') { - // allow the user to cancel empty block by hitting return, useful in conjunction with below `inBlock` - return change - .setBlocks(DEFAULT_NODE) - .unwrapBlock('bulleted-list') - .unwrapBlock('numbered-list'); - } - - // TODO strip trailing lines from blockquotes/list entries - // the below code seemingly works but doesn't account for edge cases like return with caret not at end - /* const trailingNewlines = text.match(/\n*$/); - if (trailingNewlines && trailingNewlines[0]) { - remove trailing newlines at the end of this block before making a new one - return change.deleteBackward(trailingNewlines[0].length); - }*/ - - return; - } - - let contentText; - let contentHTML; - - // only look for commands if the first block contains simple unformatted text - // i.e. no pills or rich-text formatting and begins with a /. - let cmd; let commandText; - const firstChild = editorState.document.nodes.get(0); - const firstGrandChild = firstChild && firstChild.nodes.get(0); - if (firstChild && firstGrandChild && - firstChild.object === 'block' && firstGrandChild.object === 'text' && - firstGrandChild.text[0] === '/') { - commandText = this.plainWithIdPills.serialize(editorState); - cmd = processCommandInput(this.props.room.roomId, commandText); - } - - if (cmd) { - if (!cmd.error) { - this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown'); - this.setState({ - editorState: this.createEditorState(), - }, ()=>{ - this._editor.focus(); - }); - } - if (cmd.promise) { - cmd.promise.then(()=>{ - console.log("Command success."); - }, (err)=>{ - console.error("Command failure: %s", err); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Server error', '', ErrorDialog, { - title: _t("Server error"), - description: ((err && err.message) ? err.message : _t( - "Server unavailable, overloaded, or something else went wrong.", - )), - }); - }); - } else if (cmd.error) { - console.error(cmd.error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - // TODO possibly track which command they ran (not its Arguments) here - Modal.createTrackedDialog('Command error', '', ErrorDialog, { - title: _t("Command error"), - description: cmd.error, - }); - } - return true; - } - - const replyingToEv = RoomViewStore.getQuotingEvent(); - const mustSendHTML = Boolean(replyingToEv); - - if (this.state.isRichTextEnabled) { - // We should only send HTML if any block is styled or contains inline style - let shouldSendHTML = false; - - if (mustSendHTML) shouldSendHTML = true; - - if (!shouldSendHTML) { - shouldSendHTML = !!editorState.document.findDescendant(node => { - // N.B. node.getMarks() might be private? - return ((node.object === 'block' && node.type !== 'paragraph') || - (node.object === 'inline') || - (node.object === 'text' && node.getMarks().size > 0)); - }); - } - - contentText = this.plainWithPlainPills.serialize(editorState); - if (contentText === '') return true; - - if (shouldSendHTML) { - contentHTML = HtmlUtils.processHtmlForSending(this.html.serialize(editorState)); - } - } else { - const sourceWithPills = this.plainWithMdPills.serialize(editorState); - if (sourceWithPills === '') return true; - - const mdWithPills = new Markdown(sourceWithPills); - - // if contains no HTML and we're not quoting (needing HTML) - if (mdWithPills.isPlainText() && !mustSendHTML) { - // N.B. toPlainText is only usable here because we know that the MD - // didn't contain any formatting in the first place... - contentText = mdWithPills.toPlaintext(); - } else { - // 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(); - } - } - - let sendHtmlFn = ContentHelpers.makeHtmlMessage; - let sendTextFn = ContentHelpers.makeTextMessage; - - this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown'); - - if (commandText && commandText.startsWith('/me')) { - if (replyingToEv) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Emote Reply Fail', '', ErrorDialog, { - title: _t("Unable to reply"), - description: _t("At this time it is not possible to reply with an emote."), - }); - return false; - } - - contentText = contentText.substring(4); - // bit of a hack, but the alternative would be quite complicated - if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, ''); - sendHtmlFn = ContentHelpers.makeHtmlEmote; - sendTextFn = ContentHelpers.makeEmoteMessage; - } - - let content = contentHTML ? - sendHtmlFn(contentText, contentHTML) : - sendTextFn(contentText); - - if (replyingToEv) { - const replyContent = ReplyThread.makeReplyMixIn(replyingToEv); - content = Object.assign(replyContent, content); - - // Part of Replies fallback support - prepend the text we're sending - // with the text we're replying to - const nestedReply = ReplyThread.getNestedReplyText(replyingToEv, this.props.permalinkCreator); - if (nestedReply) { - if (content.formatted_body) { - content.formatted_body = nestedReply.html + content.formatted_body; - } - content.body = nestedReply.body + content.body; - } - - // Clear reply_to_event as we put the message into the queue - // if the send fails, retry will handle resending. - dis.dispatch({ - action: 'reply_to_event', - event: null, - }); - } - - this.client.sendMessage(this.props.room.roomId, content).then((res) => { - dis.dispatch({ - action: 'message_sent', - }); - }).catch((e) => { - onSendMessageFailed(e, this.props.room); - }); - - this.setState({ - editorState: this.createEditorState(), - }, ()=>{ this._editor.focus(); }); - - return true; - }; - - onVerticalArrow = (e, up) => { - if (e.ctrlKey || e.shiftKey || e.metaKey) return; - - const shouldSelectHistory = e.altKey; - const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent(); - - if (shouldSelectHistory) { - // Try select composer history - const selected = this.selectHistory(up); - if (selected) { - // We're selecting history, so prevent the key event from doing anything else - e.preventDefault(); - } - } else if (shouldEditLastMessage) { - // selection must be collapsed - const selection = this.state.editorState.selection; - if (!selection.isCollapsed) return; - // and we must be at the edge of the document (up=start, down=end) - const document = this.state.editorState.document; - if (up) { - if (!selection.anchor.isAtStartOfNode(document)) return; - } else { - if (!selection.anchor.isAtEndOfNode(document)) return; - } - - const editEvent = findEditableEvent(this.props.room, false); - if (editEvent) { - // We're selecting history, so prevent the key event from doing anything else - e.preventDefault(); - dis.dispatch({ - action: 'edit_event', - event: editEvent, - }); - } - } - }; - - selectHistory = (up) => { - const delta = up ? -1 : 1; - - // True if we are not currently selecting history, but composing a message - if (this.historyManager.currentIndex === this.historyManager.history.length) { - // We can't go any further - there isn't any more history, so nop. - if (!up) { - return; - } - this.setState({ - currentlyComposedEditorState: this.state.editorState, - }); - } else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) { - // True when we return to the message being composed currently - this.setState({ - editorState: this.state.currentlyComposedEditorState, - }); - this.historyManager.currentIndex = this.historyManager.history.length; - return; - } - - let editorState; - const historyItem = this.historyManager.getItem(delta); - if (!historyItem) return; - - if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) { - editorState = this.richToMdEditorState(historyItem.value); - } else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) { - editorState = this.mdToRichEditorState(historyItem.value); - } else { - editorState = historyItem.value; - } - - // Move selection to the end of the selected history - const change = editorState.change().moveToEndOfNode(editorState.document); - - // We don't call this.onChange(change) now, as fixups on stuff like pills - // should already have been done and persisted in the history. - editorState = change.value; - - this.suppressAutoComplete = true; - - this.setState({ editorState }, ()=>{ - this._editor.focus(); - }); - return true; - }; - - onTab = async (e) => { - this.setState({ - someCompletions: null, - }); - e.preventDefault(); - if (this.autocomplete.countCompletions() === 0) { - // Force completions to show for the text currently entered - const completionCount = await this.autocomplete.forceComplete(); - this.setState({ - someCompletions: completionCount > 0, - }); - // Select the first item by moving "down" - await this.autocomplete.moveSelection(+1); - } else { - await this.autocomplete.moveSelection(e.shiftKey ? -1 : +1); - } - }; - - onEscape = async (e) => { - e.preventDefault(); - if (this.autocomplete) { - this.autocomplete.onEscape(e); - } - await this.setDisplayedCompletion(null); // restore originalEditorState - }; - - onAutocompleteConfirm = (displayedCompletion: ?Completion) => { - this.focusComposer(); - // XXX: this fails if the composer isn't focused so focus it and delay the completion until next tick - setImmediate(() => { - this.setDisplayedCompletion(displayedCompletion); - }); - }; - - /* If passed null, restores the original editor content from state.originalEditorState. - * If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState. - */ - setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => { - const activeEditorState = this.state.originalEditorState || this.state.editorState; - - if (displayedCompletion == null) { - if (this.state.originalEditorState) { - const editorState = this.state.originalEditorState; - this.setState({editorState}); - } - return false; - } - - const { - range = null, - completion = '', - completionId = '', - href = null, - suffix = '', - } = displayedCompletion; - - let inline; - if (href) { - inline = Inline.create({ - type: 'pill', - data: { completion, completionId, href }, - }); - } else if (completion === '@room') { - inline = Inline.create({ - type: 'pill', - data: { completion, completionId }, - }); - } - - let editorState = activeEditorState; - - if (range) { - const change = editorState.change() - .moveToAnchor() - .moveAnchorTo(range.start) - .moveFocusTo(range.end) - .focus(); - editorState = change.value; - } - - let change; - if (inline) { - change = editorState.change() - .insertInlineAtRange(editorState.selection, inline) - .insertText(suffix) - .focus(); - } else { - change = editorState.change() - .insertTextAtRange(editorState.selection, completion) - .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); - - return true; - }; - - renderNode = props => { - const { attributes, children, node, isSelected } = props; - - switch (node.type) { - case 'paragraph': - return

{children}

; - case 'block-quote': - return
{children}
; - case 'bulleted-list': - return
    {children}
; - case 'heading1': - return

{children}

; - case 'heading2': - return

{children}

; - case 'heading3': - return

{children}

; - case 'heading4': - return

{children}

; - case 'heading5': - return
{children}
; - case 'heading6': - return
{children}
; - case 'list-item': - return
  • {children}
  • ; - case 'numbered-list': - return
      {children}
    ; - case 'code': - return
    {children}
    ; - case 'link': - return {children}; - case 'pill': { - const { data } = node; - const url = data.get('href'); - const completion = data.get('completion'); - - const shouldShowPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar"); - const Pill = sdk.getComponent('elements.Pill'); - - if (completion === '@room') { - return ; - } else if (Pill.isPillUrl(url)) { - return ; - } else { - const { text } = node; - return - { text } - ; - } - } - case 'emoji': { - const { data } = node; - return data.get('emojiUnicode'); - } - } - }; - - renderMark = props => { - const { children, mark, attributes } = props; - switch (mark.type) { - case 'bold': - return {children}; - case 'italic': - return {children}; - case 'code': - return {children}; - case 'underlined': - return {children}; - case 'deleted': - return {children}; - } - }; - - onFormatButtonClicked = (name, e) => { - 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._editor.focus(); - setTimeout(()=>{ - this.handleKeyCommand(name); - }, 500); // can't find any callback to hook this to. onFocus and onChange and willComponentUpdate fire too early. - return; - } - - this.handleKeyCommand(name); - }; - - getAutocompleteQuery(editorState: Value) { - // We can just return the current block where the selection begins, which - // should be enough to capture any autocompletion input, given autocompletion - // providers only search for the first match which intersects with the current selection. - // This avoids us having to serialize the whole thing to plaintext and convert - // selection offsets in & out of the plaintext domain. - - if (editorState.selection.anchor.key) { - return editorState.document.getDescendant(editorState.selection.anchor.key).text; - } else { - return ''; - } - } - - getSelectionRange(editorState: Value) { - let beginning = false; - const firstChild = editorState.document.nodes.get(0); - const firstGrandChild = firstChild && firstChild.nodes.get(0); - beginning = (firstChild && firstGrandChild && - firstChild.object === 'block' && firstGrandChild.object === 'text' && - editorState.selection.anchor.key === firstGrandChild.key); - - // return a character range suitable for handing to an autocomplete provider. - // the range is relative to the anchor of the current editor selection. - // if the selection spans multiple blocks, then we collapse it for the calculation. - const range = { - beginning, // whether the selection is in the first block of the editor or not - start: editorState.selection.anchor.offset, - end: (editorState.selection.anchor.key == editorState.selection.focus.key) ? - editorState.selection.focus.offset : editorState.selection.anchor.offset, - }; - if (range.start > range.end) { - const tmp = range.start; - range.start = range.end; - range.end = tmp; - } - return range; - } - - onMarkdownToggleClicked = (e) => { - e.preventDefault(); // don't steal focus from the editor! - this.handleKeyCommand('toggle-mode'); - }; - - focusComposer = () => { - this._editor.focus(); - }; - - render() { - const activeEditorState = this.state.originalEditorState || this.state.editorState; - - const className = classNames('mx_MessageComposer_input', { - mx_MessageComposer_input_error: this.state.someCompletions === false, - }); - - const isEmpty = Plain.serialize(this.state.editorState) === ''; - - let {placeholder} = this.props; - // XXX: workaround for placeholder being shown when there is a formatting block e.g blockquote but no text - if (isEmpty && this.state.editorState.startBlock && this.state.editorState.startBlock.type !== DEFAULT_NODE) { - placeholder = undefined; - } - - const markdownClasses = classNames({ - mx_MessageComposer_input_markdownIndicator: true, - mx_MessageComposer_markdownDisabled: this.state.isRichTextEnabled, - }); - - return ( -
    -
    - - this.autocomplete = e} - room={this.props.room} - onConfirm={this.onAutocompleteConfirm} - onSelectionChange={this.setDisplayedCompletion} - query={ this.suppressAutoComplete ? '' : this.getAutocompleteQuery(activeEditorState) } - selection={this.getSelectionRange(activeEditorState)} - /> -
    -
    - - -
    -
    - ); - } -} diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js deleted file mode 100644 index 2b68e0d338..0000000000 --- a/src/components/views/rooms/SlateMessageComposer.js +++ /dev/null @@ -1,485 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import React, {createRef} from 'react'; -import PropTypes from 'prop-types'; -import { _t, _td } from '../../../languageHandler'; -import CallHandler from '../../../CallHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; -import sdk from '../../../index'; -import dis from '../../../dispatcher'; -import RoomViewStore from '../../../stores/RoomViewStore'; -import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; -import Stickerpicker from './Stickerpicker'; -import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks'; -import ContentMessages from '../../../ContentMessages'; - -import E2EIcon from './E2EIcon'; - -const formatButtonList = [ - _td("bold"), - _td("italic"), - _td("deleted"), - _td("underlined"), - _td("inline-code"), - _td("block-quote"), - _td("bulleted-list"), - _td("numbered-list"), -]; - -function ComposerAvatar(props) { - const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); - return
    - -
    ; -} - -ComposerAvatar.propTypes = { - me: PropTypes.object.isRequired, -} - -function CallButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const onVoiceCallClick = (ev) => { - dis.dispatch({ - action: 'place_call', - type: "voice", - room_id: props.roomId, - }); - }; - - return -} - -CallButton.propTypes = { - roomId: PropTypes.string.isRequired -} - -function VideoCallButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const onCallClick = (ev) => { - dis.dispatch({ - action: 'place_call', - type: ev.shiftKey ? "screensharing" : "video", - room_id: props.roomId, - }); - }; - - return ; -} - -VideoCallButton.propTypes = { - roomId: PropTypes.string.isRequired, -}; - -function HangupButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const onHangupClick = () => { - const call = CallHandler.getCallForRoom(props.roomId); - if (!call) { - return; - } - dis.dispatch({ - action: 'hangup', - // hangup the call for this room, which may not be the room in props - // (e.g. conferences which will hangup the 1:1 room instead) - room_id: call.roomId, - }); - }; - return ; -} - -HangupButton.propTypes = { - roomId: PropTypes.string.isRequired, -} - -function FormattingButton(props) { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ; -} - -FormattingButton.propTypes = { - showFormatting: PropTypes.bool.isRequired, - onClickHandler: PropTypes.func.isRequired, -} - -class UploadButton extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - } - constructor(props) { - super(props); - this.onUploadClick = this.onUploadClick.bind(this); - this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this); - - this._uploadInput = createRef(); - } - - onUploadClick(ev) { - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'require_registration'}); - return; - } - this._uploadInput.current.click(); - } - - onUploadFileInputChange(ev) { - if (ev.target.files.length === 0) return; - - // take a copy so we can safely reset the value of the form control - // (Note it is a FileList: we can't use slice or sesnible iteration). - const tfiles = []; - for (let i = 0; i < ev.target.files.length; ++i) { - tfiles.push(ev.target.files[i]); - } - - ContentMessages.sharedInstance().sendContentListToRoom( - tfiles, this.props.roomId, MatrixClientPeg.get(), - ); - - // This is the onChange handler for a file form control, but we're - // not keeping any state, so reset the value of the form control - // to empty. - // NB. we need to set 'value': the 'files' property is immutable. - ev.target.value = ''; - } - - render() { - const uploadInputStyle = {display: 'none'}; - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return ( - - - - ); - } -} - -export default class SlateMessageComposer extends React.Component { - constructor(props) { - super(props); - this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); - this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this); - this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); - this.onInputStateChanged = this.onInputStateChanged.bind(this); - this.onEvent = this.onEvent.bind(this); - this._onRoomStateEvents = this._onRoomStateEvents.bind(this); - this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); - this._onTombstoneClick = this._onTombstoneClick.bind(this); - this.renderPlaceholderText = this.renderPlaceholderText.bind(this); - this.renderFormatBar = this.renderFormatBar.bind(this); - - this.state = { - inputState: { - marks: [], - blockType: null, - isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'), - }, - showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'), - isQuoting: Boolean(RoomViewStore.getQuotingEvent()), - tombstone: this._getRoomTombstone(), - canSendMessages: this.props.room.maySendMessage(), - }; - } - - componentDidMount() { - // N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler - // for 'event' fires *after* 'RoomEvent', and our room won't have yet been - // marked as encrypted. - // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. - MatrixClientPeg.get().on("event", this.onEvent); - MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents); - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); - this._waitForOwnMember(); - } - - _waitForOwnMember() { - // if we have the member already, do that - const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); - if (me) { - this.setState({me}); - return; - } - // Otherwise, wait for member loading to finish and then update the member for the avatar. - // The members should already be loading, and loadMembersIfNeeded - // will return the promise for the existing operation - this.props.room.loadMembersIfNeeded().then(() => { - const me = this.props.room.getMember(MatrixClientPeg.get().getUserId()); - this.setState({me}); - }); - } - - componentWillUnmount() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("event", this.onEvent); - MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents); - } - if (this._roomStoreToken) { - this._roomStoreToken.remove(); - } - } - - onEvent(event) { - if (event.getType() !== 'm.room.encryption') return; - if (event.getRoomId() !== this.props.room.roomId) return; - this.forceUpdate(); - } - - _onRoomStateEvents(ev, state) { - if (ev.getRoomId() !== this.props.room.roomId) return; - - if (ev.getType() === 'm.room.tombstone') { - this.setState({tombstone: this._getRoomTombstone()}); - } - if (ev.getType() === 'm.room.power_levels') { - this.setState({canSendMessages: this.props.room.maySendMessage()}); - } - } - - _getRoomTombstone() { - return this.props.room.currentState.getStateEvents('m.room.tombstone', ''); - } - - _onRoomViewStoreUpdate() { - const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); - if (this.state.isQuoting === isQuoting) return; - this.setState({ isQuoting }); - } - - - onInputStateChanged(inputState) { - // Merge the new input state with old to support partial updates - inputState = Object.assign({}, this.state.inputState, inputState); - this.setState({inputState}); - } - - _onAutocompleteConfirm(range, completion) { - if (this.messageComposerInput) { - this.messageComposerInput.setDisplayedCompletion(range, completion); - } - } - - onFormatButtonClicked(name, event) { - event.preventDefault(); - this.messageComposerInput.onFormatButtonClicked(name, event); - } - - onToggleFormattingClicked() { - SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting); - this.setState({showFormatting: !this.state.showFormatting}); - } - - onToggleMarkdownClicked(e) { - e.preventDefault(); // don't steal focus from the editor! - this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled); - } - - _onTombstoneClick(ev) { - ev.preventDefault(); - - const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; - const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId); - let createEventId = null; - if (replacementRoom) { - const createEvent = replacementRoom.currentState.getStateEvents('m.room.create', ''); - if (createEvent && createEvent.getId()) createEventId = createEvent.getId(); - } - - const viaServers = [this.state.tombstone.getSender().split(':').splice(1).join(':')]; - dis.dispatch({ - action: 'view_room', - highlighted: true, - event_id: createEventId, - room_id: replacementRoomId, - auto_join: true, - - // Try to join via the server that sent the event. This converts @something:example.org - // into a server domain by splitting on colons and ignoring the first entry ("@something"). - via_servers: viaServers, - opts: { - // These are passed down to the js-sdk's /join call - viaServers: viaServers, - }, - }); - } - - renderPlaceholderText() { - const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); - if (this.state.isQuoting) { - if (roomIsEncrypted) { - return _t('Send an encrypted reply…'); - } else { - return _t('Send a reply (unencrypted)…'); - } - } else { - if (roomIsEncrypted) { - return _t('Send an encrypted message…'); - } else { - return _t('Send a message (unencrypted)…'); - } - } - } - - renderFormatBar() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const {marks, blockType} = this.state.inputState; - const formatButtons = formatButtonList.map((name) => { - // special-case to match the md serializer and the special-case in MessageComposerInput.js - const markName = name === 'inline-code' ? 'code' : name; - const active = marks.some(mark => mark.type === markName) || blockType === name; - const suffix = active ? '-on' : ''; - const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); - const className = 'mx_MessageComposer_format_button mx_filterFlipColor'; - return ( - - ); - }) - - return ( -
    -
    - { formatButtons } -
    - - -
    -
    - ); - } - - render() { - const controls = [ - this.state.me ? : null, - this.props.e2eStatus ? : null, - ]; - - if (!this.state.tombstone && this.state.canSendMessages) { - // This also currently includes the call buttons. Really we should - // check separately for whether we can call, but this is slightly - // complex because of conference calls. - - const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); - const showFormattingButton = this.state.inputState.isRichTextEnabled; - const callInProgress = this.props.callState && this.props.callState !== 'ended'; - - controls.push( - this.messageComposerInput = c} - key="controls_input" - room={this.props.room} - placeholder={this.renderPlaceholderText()} - onInputStateChanged={this.onInputStateChanged} - permalinkCreator={this.props.permalinkCreator} />, - showFormattingButton ? : null, - , - , - callInProgress ? : null, - callInProgress ? null : , - callInProgress ? null : , - ); - } else if (this.state.tombstone) { - const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; - - const continuesLink = replacementRoomId ? ( - - {_t("The conversation continues here.")} - - ) : ''; - - controls.push(
    -
    - - - {_t("This room has been replaced and is no longer active.")} -
    - { continuesLink } -
    -
    ); - } else { - controls.push( -
    - { _t('You do not have permission to post to this room') } -
    , - ); - } - - const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled; - - return ( -
    -
    -
    - { controls } -
    -
    - { showFormatBar ? this.renderFormatBar() : null } -
    - ); - } -} - -SlateMessageComposer.propTypes = { - // js-sdk Room object - room: PropTypes.object.isRequired, - - // string representing the current voip call state - callState: PropTypes.string, - - // string representing the current room app drawer state - showApps: PropTypes.bool -}; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index 6fc854c155..a8a393887b 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -26,7 +26,6 @@ import PlatformPeg from "../../../../../PlatformPeg"; export default class PreferencesUserSettingsTab extends React.Component { static COMPOSER_SETTINGS = [ - 'useCiderComposer', 'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.suggestEmoji', 'sendTypingNotifications', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5d32066fd8..9c2105afab 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -362,7 +362,6 @@ "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Show info about bridges in room settings": "Show info about bridges in room settings", - "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", @@ -956,13 +955,6 @@ "Strikethrough": "Strikethrough", "Code block": "Code block", "Quote": "Quote", - "Server error": "Server error", - "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", - "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.", - "Markdown is disabled": "Markdown is disabled", - "Markdown is enabled": "Markdown is enabled", "No pinned messages.": "No pinned messages.", "Loading...": "Loading...", "Pinned Messages": "Pinned Messages", @@ -1064,16 +1056,9 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", - "bold": "bold", - "italic": "italic", - "deleted": "deleted", - "underlined": "underlined", - "inline-code": "inline-code", - "block-quote": "block-quote", - "bulleted-list": "bulleted-list", - "numbered-list": "numbered-list", - "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", - "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", + "Server error": "Server error", + "Command error": "Command error", + "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 1d24e81469..14208e1f03 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -161,11 +161,6 @@ export const SETTINGS = { displayName: _td("Show info about bridges in room settings"), default: false, }, - "useCiderComposer": { - displayName: _td("Use the new, faster, composer for writing messages"), - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: true, - }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js deleted file mode 100644 index ab2dbfedec..0000000000 --- a/src/stores/MessageComposerStore.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -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. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import { Value } from 'slate'; - -const localStoragePrefix = 'editor_state_'; - -/** - * A class for storing application state to do with the message composer (specifically in-progress message drafts). - * It does not worry about cleaning up on log out as this is handled in Lifecycle.js by localStorage.clear() - */ -class MessageComposerStore { - constructor() { - this.prefix = localStoragePrefix; - } - - _getKey(roomId: string): string { - return this.prefix + roomId; - } - - setEditorState(roomId: string, editorState: Value, richText: boolean) { - localStorage.setItem(this._getKey(roomId), JSON.stringify({ - editor_state: editorState.toJSON({ - preserveSelection: true, - // XXX: re-hydrating history is not currently supported by fromJSON - // preserveHistory: true, - // XXX: this seems like a workaround for selection.isSet being based on anchorKey instead of anchorPath - preserveKeys: true, - }), - rich_text: richText, - })); - } - - getEditorState(roomId): {editor_state: Value, rich_text: boolean} { - const stateStr = localStorage.getItem(this._getKey(roomId)); - - let state; - if (stateStr) { - state = JSON.parse(stateStr); - - // if it does not have the fields we expect then bail - if (!state || state.rich_text === undefined || state.editor_state === undefined) return; - state.editor_state = Value.fromJSON(state.editor_state); - } - - return state; - } -} - -let singletonMessageComposerStore = null; -if (!singletonMessageComposerStore) { - singletonMessageComposerStore = new MessageComposerStore(); -} -module.exports = singletonMessageComposerStore; diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 60380eecd2..518e7e06ac 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -19,7 +19,7 @@ function addTextToDraft(text) { } } -// FIXME: These tests need to be updated from Draft to Slate. +// FIXME: These tests need to be updated from Draft to CIDER. xdescribe('MessageComposerInput', () => { let parentDiv = null, diff --git a/yarn.lock b/yarn.lock index fc2b9e04c4..a491ba3941 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2592,11 +2592,6 @@ dir-glob@^2.2.2: dependencies: path-type "^3.0.0" -direction@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/direction/-/direction-0.1.5.tgz#ce5d797f97e26f8be7beff53f7dc40e1c1a9ec4c" - integrity sha1-zl15f5fib4vnvv9T99xA4cGp7Ew= - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3065,11 +3060,6 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" -esrever@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/esrever/-/esrever-0.2.0.tgz#96e9d28f4f1b1a76784cd5d490eaae010e7407b8" - integrity sha1-lunSj08bGnZ4TNXUkOquAQ50B7g= - estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" @@ -3653,11 +3643,6 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-document@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-document/-/get-document-1.0.0.tgz#4821bce66f1c24cb0331602be6cb6b12c4f01c4b" - integrity sha1-SCG85m8cJMsDMWAr5strEsTwHEs= - get-stdin@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" @@ -3675,13 +3660,6 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -get-window@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/get-window/-/get-window-1.1.2.tgz#65fbaa999fb87f86ea5d30770f4097707044f47f" - integrity sha512-yjWpFcy9fjhLQHW1dPtg9ga4pmizLY8y4ZSHdGrAQ1NU277MRhnGnnLPxe19X8W5lWVsCZz++5xEuNozWMVmTw== - dependencies: - get-document "1" - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -4509,16 +4487,6 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.3.tgz#e8a426a69b6d31470d3a33a47bb825cda02506ee" integrity sha512-zxQ9//Q3D/34poZf8fiy3m3XVpbQc7ren15iKqrTtLPwkPD/t3Scy9Imp63FujULGxuK0ZlCwoo5xNpktFgbOA== -is-hotkey@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.4.tgz#c34d2c85d6ec8d09a871dcf71931c8067a824c7d" - integrity sha512-Py+aW4r5mBBY18TGzGz286/gKS+fCQ0Hee3qkaiSmEPiD0PqFpe0wuA3l7rTOUKyeXl8Mxf3XzJxIoTlSv+kxA== - -is-in-browser@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" - integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= - is-ip@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-2.0.0.tgz#68eea07e8a0a0a94c2d080dd674c731ab2a461ab" @@ -4651,11 +4619,6 @@ is-whitespace-character@^1.0.0: resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.3.tgz#b3ad9546d916d7d3ffa78204bca0c26b56257fac" integrity sha512-SNPgMLz9JzPccD3nPctcj8sZlX9DAMJSKH8bP7Z6bohCwuNgX8xbWr1eTAYXX9Vpi/aSn8Y1akL9WgM3t43YNQ== -is-window@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" - integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0= - is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -4715,11 +4678,6 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -isomorphic-base64@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/isomorphic-base64/-/isomorphic-base64-1.0.2.tgz#f426aae82569ba8a4ec5ca73ad21a44ab1ee7803" - integrity sha1-9Caq6CVpuopOxcpzrSGkSrHueAM= - isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" @@ -5154,7 +5112,7 @@ lodash.unescape@4.0.1: resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= -lodash@^4.1.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -5379,11 +5337,6 @@ memoize-one@^3.0.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== -memoize-one@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" - integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== - memory-fs@^0.4.0, memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -6715,11 +6668,6 @@ react-focus-lock@^2.2.1: dependencies: gemini-scrollbar matrix-org/gemini-scrollbar#91e1e566 -react-immutable-proptypes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" - integrity sha1-Aj1vObsVyXwHHp5g0A0TbqxfoLQ= - react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" @@ -7271,11 +7219,6 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -selection-is-backward@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/selection-is-backward/-/selection-is-backward-1.0.0.tgz#97a54633188a511aba6419fc5c1fa91b467e6be1" - integrity sha1-l6VGMxiKURq6ZBn8XB+pG0Z+a+E= - "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -7371,93 +7314,6 @@ slash@^2.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== -slate-base64-serializer@^0.2.69: - version "0.2.112" - resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.112.tgz#791d04a0ae7b9796844f068a904e185f2afc91f9" - integrity sha512-Vo94bkCq8cbFj7Lutdh2RaM9S4WlLxnnMqZPKGUyefklUN4q2EzM/WUH7s9CIlLUH1qRfC/b0V25VJZr5XXTzA== - dependencies: - isomorphic-base64 "^1.0.2" - -slate-dev-environment@^0.2.0, slate-dev-environment@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.2.2.tgz#bd8946e1fe4cf5447060c84a362a1d026ed8b77f" - integrity sha512-JZ09llrRQu6JUsLJCUlGC0lB1r1qIAabAkSd454iyYBq6lDuY//Bypi3Jo8yzIfzZ4+mRLdQvl9e8MbeM9l48Q== - dependencies: - is-in-browser "^1.1.3" - -slate-dev-logger@^0.1.43: - version "0.1.43" - resolved "https://registry.yarnpkg.com/slate-dev-logger/-/slate-dev-logger-0.1.43.tgz#77f6ca7207fcbf453a5516f3aa8b19794d1d26dc" - integrity sha512-GkcPMGzmPVm85AL+jaKnzhIA0UH9ktQDEIDM+FuQtz+TAPcpPCQiRAaZ6I2p2uD0Hq9bImhKSCtHIa0qRxiVGw== - -slate-dev-warning@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/slate-dev-warning/-/slate-dev-warning-0.0.1.tgz#f6c36731babea5e301b5bd504fe64911dd24200a" - integrity sha512-QdXa+qmOG46VrTfnzn2gUVzs1WiO3Q+zCv3XomzMNGdgAJjCgHBs3jaeQD845h15loS3OJ181gCNAkB3dby6Hw== - -slate-hotkeys@^0.2.5: - version "0.2.9" - resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.2.9.tgz#0cc9eb750a49ab9ef11601305b7c82b5402348e3" - integrity sha512-y+C/s5vJEmBxo8fIqHmUcdViGwALL/A6Qow3sNG1OHYD5SI11tC2gfYtGbPh+2q0H7O4lufffCmFsP5bMaDHqA== - dependencies: - is-hotkey "0.1.4" - slate-dev-environment "^0.2.2" - -slate-html-serializer@^0.6.1: - version "0.6.32" - resolved "https://registry.yarnpkg.com/slate-html-serializer/-/slate-html-serializer-0.6.32.tgz#69b0fcdb89a0bdcea28b60b6a90b944651ad3277" - integrity sha512-x1RP1R2HMaVFflk9UXiuepcbN4wMoJRv0VWtxFw8efGNFmJfNBWME4iXAy6GNFRV0rRPlG3xCuQv2wHZ/+JMYw== - dependencies: - slate-dev-logger "^0.1.43" - type-of "^2.0.1" - -"slate-md-serializer@github:matrix-org/slate-md-serializer#f7c4ad3": - version "3.1.0" - resolved "https://codeload.github.com/matrix-org/slate-md-serializer/tar.gz/f7c4ad394f5af34d4c623de7909ce95ab78072d3" - -slate-plain-serializer@^0.6.8: - version "0.6.39" - resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.6.39.tgz#5fb8d4dc530a2e7e0689548d48964ce242c4516a" - integrity sha512-EGl+Y+9Fw9IULtPg8sttydaeiAoaibJolMXNfqI79+5GWTQwJFIbg24keKvsTw+3f2RieaPu8fcrKyujKtZ7ZQ== - -slate-prop-types@^0.4.67: - version "0.4.67" - resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.67.tgz#c6aa74195466546a44fcb85d1c7b15fefe36ce6b" - integrity sha512-FmdwitAw1Y69JHm326dfwP6Zd6R99jz1Im8jvKcnG2hytk72I1vIv6ct2CkNGwc3sg90+OIO/Rf18frYxxoTzw== - -slate-react@^0.18.10: - version "0.18.11" - resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.18.11.tgz#f452e7eb73f0271422d2a17e8090dcd8d889aef6" - integrity sha512-7u0+LLabGaxjWYb0oTqUDcs3iCvJdaZwcGW6hLc1hFv06KkwaIxAqYpP8dUBRVlQd+0/X0TdyagCmf0IjFSPhg== - dependencies: - debug "^3.1.0" - get-window "^1.1.1" - is-window "^1.0.2" - lodash "^4.1.1" - memoize-one "^4.0.0" - prop-types "^15.5.8" - react-immutable-proptypes "^2.1.0" - selection-is-backward "^1.0.0" - slate-base64-serializer "^0.2.69" - slate-dev-environment "^0.2.0" - slate-dev-warning "^0.0.1" - slate-hotkeys "^0.2.5" - slate-plain-serializer "^0.6.8" - slate-prop-types "^0.4.67" - -slate@^0.41.2: - version "0.41.3" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.41.3.tgz#fa468de5db53afc453a0a7d7875b4de05737a900" - integrity sha512-I/ymHWRxtoSOWYKh/SFgW3Vkkojt5ywWf7Wh4oBvaKic/3mAsM1wymyZmhnvSKK59IeE0JJzD4uyyQaM1KEFHA== - dependencies: - debug "^3.1.0" - direction "^0.1.5" - esrever "^0.2.0" - is-plain-object "^2.0.4" - lodash "^4.17.4" - slate-dev-warning "^0.0.1" - type-of "^2.0.1" - slice-ansi@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" @@ -8264,11 +8120,6 @@ type-is@~1.6.17: media-typer "0.3.0" mime-types "~2.1.24" -type-of@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972" - integrity sha1-5yoXQYllaOn2KDeNgW1pEvfyOXI= - typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"