diff --git a/package.json b/package.json index 111fb97387..f79e0abd82 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "react-dom": "^15.6.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "resize-observer-polyfill": "^1.5.0", - "slate": "0.33.4", + "slate": "0.34.7", "slate-react": "^0.12.4", "slate-html-serializer": "^0.6.1", "slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3", diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index 732ada088b..3e1016f60d 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -69,7 +69,8 @@ flex-flow: wrap; } -.mx_Autocomplete_Completion.selected { +.mx_Autocomplete_Completion.selected, +.mx_Autocomplete_Completion:hover { background: $menu-bg-color; outline: none; } diff --git a/src/RichText.js b/src/RichText.js index 65b5dad107..3e8f834da6 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -16,37 +16,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; - -import * as sdk from './index'; import * as emojione from 'emojione'; -import { SelectionRange } from "./autocomplete/Autocompleter"; - export function unicodeToEmojiUri(str) { - let replaceWith, unicode, alt; - if ((!emojione.unicodeAlt) || (emojione.sprites)) { - // if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames - const mappedUnicode = emojione.mapUnicodeToShort(); - } + const mappedUnicode = emojione.mapUnicodeToShort(); - str = str.replace(emojione.regUnicode, function(unicodeChar) { - if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) { - // if the unicodeChar doesnt exist just return the entire match + // remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them + return str.replace(emojione.regUnicode, function(unicodeChar) { + if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) { + // if the unicodeChar doesn't exist just return the entire match return unicodeChar; } else { - // Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below - if (unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') { - unicodeChar = unicodeChar[0]; - } - // get the unicode codepoint from the actual char - unicode = emojione.jsEscapeMap[unicodeChar]; + const unicode = emojione.jsEscapeMap[unicodeChar]; - return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam; + const short = mappedUnicode[unicode]; + const fname = emojione.emojioneList[short].fname; + + return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam; } }); - - return str; } diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 9ebac031a8..a35a31966a 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -48,7 +48,7 @@ export default class CommandProvider extends AutocompleteProvider { const name = command[1].substr(1); // strip leading `/` if (CommandMap[name]) { // some commands, namely `me` and `ddg` don't suit having the usage shown whilst typing their arguments - if (!CommandMap[name].hideCompletionAfterSpace) return []; + if (CommandMap[name].hideCompletionAfterSpace) return []; matches = [CommandMap[name]]; } } else { diff --git a/src/autocomplete/PlainWithPillsSerializer.js b/src/autocomplete/PlainWithPillsSerializer.js index c1194ae2e1..59cf1bde3b 100644 --- a/src/autocomplete/PlainWithPillsSerializer.js +++ b/src/autocomplete/PlainWithPillsSerializer.js @@ -64,13 +64,17 @@ class PlainWithPillsSerializer { } 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 node.data.get('completion'); + return completion; case 'md': - return `[${ node.data.get('completion') }](${ node.data.get('href') })`; + return `[${ completion }](${ node.data.get('href') })`; case 'id': - return node.data.get('completionId') || node.data.get('completion'); + return node.data.get('completionId') || completion; } } else if (node.nodes) { return node.nodes.map(this._serializeNode).join(''); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 2e5fe2aabc..17281dae09 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -203,7 +203,7 @@ module.exports = React.createClass({ // update the current node with one that's now taken its place node = pillContainer; } - } else if (node.nodeType == Node.TEXT_NODE) { + } else if (node.nodeType === Node.TEXT_NODE) { const Pill = sdk.getComponent('elements.Pill'); let currentTextNode = node; @@ -232,6 +232,12 @@ module.exports = React.createClass({ if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) { // Now replace all those nodes with Pills for (const roomNotifTextNode of roomNotifTextNodes) { + // Set the next node to be processed to the one after the node + // we're adding now, since we've just inserted nodes into the structure + // we're iterating over. + // Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once + node = roomNotifTextNode.nextSibling; + const pillContainer = document.createElement('span'); const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const pill = 0 so we'll do this at least once - node = roomNotifTextNode.nextSibling; } // Nothing else to do for a text node (and we don't need to advance // the loop pointer because we did it above) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 4bc91b5c2b..ee6cc66d2d 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -216,12 +216,12 @@ export default class Autocomplete extends React.Component { return done.promise; } - onCompletionClicked(): boolean { - if (this.countCompletions() === 0 || this.state.selectionOffset === COMPOSER_SELECTED) { + onCompletionClicked(selectionOffset: number): boolean { + if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) { return false; } - this.props.onConfirm(this.state.completionList[this.state.selectionOffset - 1]); + this.props.onConfirm(this.state.completionList[selectionOffset - 1]); this.hide(); return true; @@ -264,8 +264,7 @@ export default class Autocomplete extends React.Component { position++; const onClick = () => { - this.setSelection(componentPosition); - this.onCompletionClicked(); + this.onCompletionClicked(componentPosition); }; return React.cloneElement(completion.component, { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index c117bce6a2..f455db7fbc 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -387,17 +387,28 @@ export default class MessageComposerInput extends React.Component { 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 { + } else { // insert it into the middle of the block (splitting it) change = change.insertBlock(quote); } - change = change.insertFragmentByKey(quote.key, 0, fragment.document) - .focus(); + + // 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({ + anchorKey: quote.key, + focusKey: quote.key, + }).collapseToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus(); + this.onChange(change); - } - else { + } else { let fragmentChange = fragment.change(); fragmentChange.moveToRangeOf(fragment.document) .wrapBlock(quote); @@ -1301,6 +1312,14 @@ export default class MessageComposerInput extends React.Component { 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. */ @@ -1563,7 +1582,7 @@ export default class MessageComposerInput extends React.Component { this.autocomplete = e} room={this.props.room} - onConfirm={this.setDisplayedCompletion} + onConfirm={this.onAutocompleteConfirm} onSelectionChange={this.setDisplayedCompletion} query={ this.suppressAutoComplete ? '' : this.getAutocompleteQuery(activeEditorState) } selection={this.getSelectionRange(activeEditorState)}