From 55e1202c0951f41be97e38992140456a43e4e634 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 13 Jul 2017 13:26:13 +0100 Subject: [PATCH 1/5] Decorate pasted links so that they look like links By default, draftjs will represent pasted `` tags as `LINK` entities, but it doesn't do any default decoration of these links. Add a decorator to do so. Most of this was taken from https://github.com/facebook/draft-js/blob/v0.8.1/examples/link/link.html (note the version, v0.8.1). --- .../views/rooms/MessageComposerInput.js | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index cf6dfbb6b7..8ba5289eb4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -16,9 +16,9 @@ limitations under the License. import React from 'react'; import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent'; -import {Editor, EditorState, RichUtils, CompositeDecorator, - convertFromRaw, convertToRaw, Modifier, EditorChangeType, - getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js'; +import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier, + getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState, + Entity} from 'draft-js'; import classNames from 'classnames'; import escape from 'lodash/escape'; @@ -144,15 +144,37 @@ export default class MessageComposerInput extends React.Component { this.client = MatrixClientPeg.get(); } + findLinkEntities(contentBlock, callback) { + contentBlock.findEntityRanges( + (character) => { + const entityKey = character.getEntity(); + return ( + entityKey !== null && + Entity.get(entityKey).getType() === 'LINK' + ); + }, callback, + ); + } /* * "Does the right thing" to create an EditorState, based on: * - whether we've got rich text mode enabled * - contentState was passed in */ createEditorState(richText: boolean, contentState: ?ContentState): EditorState { - let decorators = richText ? RichText.getScopedRTDecorators(this.props) : - RichText.getScopedMDDecorators(this.props), - compositeDecorator = new CompositeDecorator(decorators); + const decorators = richText ? RichText.getScopedRTDecorators(this.props) : + RichText.getScopedMDDecorators(this.props); + decorators.push({ + strategy: this.findLinkEntities.bind(this), + component: (props) => { + const {url} = Entity.get(props.entityKey).getData(); + return ( + + {props.children} + + ); + }, + }); + const compositeDecorator = new CompositeDecorator(decorators); let editorState = null; if (contentState) { From 4b969634083bb370da4f9457099fe6a03b8eb541 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 13 Jul 2017 13:27:49 +0100 Subject: [PATCH 2/5] Send HTML if there are any entities present in the composer This is so that pasted HTML links that are represented as entities are sent as HTML. --- src/components/views/rooms/MessageComposerInput.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 8ba5289eb4..5149d5a790 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -591,6 +591,14 @@ export default class MessageComposerInput extends React.Component { } }); } + if (!shouldSendHTML) { + const hasAnEntity = blocks.some((block) => { + return block.getCharacterList().filter((c) => { + return c.getEntity(); + }).size > 0; + }); + shouldSendHTML = hasAnEntity; + } if (shouldSendHTML) { contentHTML = HtmlUtils.processHtmlForSending( RichText.contentStateToHTML(contentState), From be045a6dc04d72cb01b3e3ec19d5d28cb6f15cbc Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 13 Jul 2017 13:28:51 +0100 Subject: [PATCH 3/5] Interpret whitespace after entity as the end of the entity The easiest way to stop the user from inserting whitespace onto the end of an entity is to toggle the entity state of the whitespace that was just entered. This allows the user to continue drafting a message without editing the link content. This is for pasted `` tags that have been copied from a website. We probably also want to be storing entities for substrings of content that are determined to be URLs. --- .../views/rooms/MessageComposerInput.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 5149d5a790..9886f8623f 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -329,6 +329,34 @@ export default class MessageComposerInput extends React.Component { onEditorContentChanged = (editorState: EditorState) => { editorState = RichText.attachImmutableEntitiesToEmoji(editorState); + 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); + } + /* Since a modification was made, set originalEditorState to null, since newState is now our original */ this.setState({ editorState, From 5826b6f22a3816bc7c1ed06794384266f411aaa2 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 13 Jul 2017 13:41:17 +0100 Subject: [PATCH 4/5] Instead of sending HTML for any Entity, only send HTML for Links Otherwise emoji messages are sent as HTML, needlessly --- src/components/views/rooms/MessageComposerInput.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 9886f8623f..c0cc937e24 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -620,12 +620,13 @@ export default class MessageComposerInput extends React.Component { }); } if (!shouldSendHTML) { - const hasAnEntity = blocks.some((block) => { + const hasLink = blocks.some((block) => { return block.getCharacterList().filter((c) => { - return c.getEntity(); + const entityKey = c.getEntity(); + return entityKey && Entity.get(entityKey).getType() === 'LINK'; }).size > 0; }); - shouldSendHTML = hasAnEntity; + shouldSendHTML = hasLink; } if (shouldSendHTML) { contentHTML = HtmlUtils.processHtmlForSending( From f1a4209d6be16012bf4044ed0f9d13e36f782ab1 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Thu, 13 Jul 2017 13:47:08 +0100 Subject: [PATCH 5/5] Fix indentation --- src/components/views/rooms/MessageComposerInput.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index c0cc937e24..452f25fd2e 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -164,13 +164,13 @@ export default class MessageComposerInput extends React.Component { const decorators = richText ? RichText.getScopedRTDecorators(this.props) : RichText.getScopedMDDecorators(this.props); decorators.push({ - strategy: this.findLinkEntities.bind(this), - component: (props) => { + strategy: this.findLinkEntities.bind(this), + component: (props) => { const {url} = Entity.get(props.entityKey).getData(); return ( - - {props.children} - + + {props.children} + ); }, });