diff --git a/src/RichText.js b/src/RichText.js index 16e16c5b81..c060565e2f 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -113,31 +113,6 @@ let emojiDecorator = { * Returns a composite decorator which has access to provided scope. */ export function getScopedRTDecorators(scope: any): CompositeDecorator { - let MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - - let usernameDecorator = { - strategy: (contentBlock, callback) => { - findWithRegex(USERNAME_REGEX, contentBlock, callback); - }, - component: (props) => { - let member = scope.room.getMember(props.children[0].props.text); - // unused until we make these decorators immutable (autocomplete needed) - let name = member ? member.name : null; - let avatar = member ? : null; - return {avatar}{props.children}; - } - }; - - let roomDecorator = { - strategy: (contentBlock, callback) => { - findWithRegex(ROOM_REGEX, contentBlock, callback); - }, - component: (props) => { - return {props.children}; - } - }; - - // TODO Re-enable usernameDecorator and roomDecorator return [emojiDecorator]; } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index ef8a39edaf..cadbe44bf6 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -118,7 +118,7 @@ export default class EmojiProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
+ return
{completions}
; } diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js index 07398e7a5f..762b285685 100644 --- a/src/autocomplete/QueryMatcher.js +++ b/src/autocomplete/QueryMatcher.js @@ -18,7 +18,7 @@ limitations under the License. import _at from 'lodash/at'; import _flatMap from 'lodash/flatMap'; import _sortBy from 'lodash/sortBy'; -import _sortedUniq from 'lodash/sortedUniq'; +import _uniq from 'lodash/uniq'; import _keys from 'lodash/keys'; class KeyMap { @@ -101,7 +101,7 @@ export default class QueryMatcher { } }); - return _sortedUniq(_flatMap(_sortBy(results, (candidate) => { + return _uniq(_flatMap(_sortBy(results, (candidate) => { return candidate.index; }).map((candidate) => { // return an array of objects (those given to setObjects) that have the given diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 2c50a94a6a..ab1d67a218 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -30,6 +30,8 @@ import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; import UserSettingsStore from "../../../UserSettingsStore"; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import {RoomMember} from 'matrix-js-sdk'; linkifyMatrix(linkify); @@ -80,6 +82,10 @@ module.exports = React.createClass({ componentDidMount: function() { this._unmounted = false; + // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer + // are still sent as plaintext URLs. If these are ever pillified in the composer, + // we should be pillify them here by doing the linkifying BEFORE the pillifying. + this.pillifyLinks(this.refs.content.children); linkifyElement(this.refs.content, linkifyMatrix.options); this.calculateUrlPreview(); @@ -162,6 +168,55 @@ module.exports = React.createClass({ } }, + pillifyLinks: function(nodes) { + const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (node.tagName === "A" && node.getAttribute("href")) { + const href = node.getAttribute("href"); + // HtmlUtils transforms `matrix.to` links to local links, so match against + // user or room app links. + const match = /^#\/(user|room)\/(.*)$/.exec(href) || []; + const resourceType = match[1]; // "user" or "room" + const resourceId = match[2]; // user ID or room ID + if (match && resourceType && resourceId) { + let avatar; + let roomId; + let room; + let member; + switch (resourceType) { + case "user": + roomId = this.props.mxEvent.getRoomId(); + room = MatrixClientPeg.get().getRoom(roomId); + member = room.getMember(resourceId) || + new RoomMember(null, resourceId); + avatar = ; + break; + case "room": + room = resourceId[0] === '#' ? + MatrixClientPeg.get().getRooms().find((r) => { + return r.getCanonicalAlias() === resourceId; + }) : MatrixClientPeg.get().getRoom(resourceId); + if (room) { + avatar = ; + } + break; + } + if (avatar) { + const avatarContainer = document.createElement('span'); + node.className = "mx_MTextBody_pill " + + (resourceType === "user" ? "mx_UserPill" : "mx_RoomPill"); + ReactDOM.render(avatar, avatarContainer); + node.insertBefore(avatarContainer, node.firstChild); + } + } + } else if (node.children && node.children.length) { + this.pillifyLinks(node.children); + } + } + }, + findLinks: function(nodes) { var links = []; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 6704998c05..17f7e44624 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -527,15 +527,18 @@ export default class MessageComposerInput extends React.Component { if (this.state.isRichtextEnabled) { // These are block types, not handled by RichUtils by default. const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item']; + const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState); if (blockCommands.includes(command)) { - this.setState({ - editorState: RichUtils.toggleBlockType(this.state.editorState, command), - }); + newState = RichUtils.toggleBlockType(this.state.editorState, command); } else if (command === 'strike') { // this is the only inline style not handled by Draft by default - this.setState({ - editorState: RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'), - }); + newState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'); + } else if (command === 'backspace' && currentBlockType !== 'unstyled') { + const currentStartOffset = this.state.editorState.getSelection().getStartOffset(); + if (currentStartOffset === 0) { + // Toggle current block type (setting it to 'unstyled') + newState = RichUtils.toggleBlockType(this.state.editorState, currentBlockType); + } } } else { const contentState = this.state.editorState.getCurrentContent(); @@ -655,6 +658,7 @@ export default class MessageComposerInput extends React.Component { // By returning false, we allow the default draft-js key binding to occur, // which in this case invokes "split-block". This creates a new block of the // same type, allowing the user to delete it with backspace. + // See handleKeyCommand (when command === 'backspace') return false; }