From 8ef820054d2135f08ff121fb8cb3e0e6d77ca693 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 21 Jul 2017 13:41:23 +0100 Subject: [PATCH] Factor out shared logic in two code-paths for pill rendering This isn't an entirely side-effect-free refactoring: - the text of the timeline pills is now either the room ID/alias or user ID/ display name of the linked resource (which means that until we do a roundtrip to get user displaynames, mentions for users not in the current room will have their user IDs shown instead of what was in the link body). - timeline links to rooms without avatars are now rendered as links - fixed issue that would throw an error whilst rendering (i.e. unusable client) a room link to a room that the client doesn't know about --- src/components/views/elements/Pill.js | 101 ++++++++++++++++++ src/components/views/messages/TextualBody.js | 57 ++-------- .../views/rooms/MessageComposerInput.js | 57 +--------- 3 files changed, 115 insertions(+), 100 deletions(-) create mode 100644 src/components/views/elements/Pill.js diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js new file mode 100644 index 0000000000..6ca97359b9 --- /dev/null +++ b/src/components/views/elements/Pill.js @@ -0,0 +1,101 @@ + +import React from 'react'; +import sdk from '../../../index'; +import classNames from 'classnames'; +import { Room, RoomMember } from 'matrix-js-sdk'; +import PropTypes from 'prop-types'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix'; +import { getDisplayAliasForRoom } from '../../../Rooms'; + +const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); +const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+]).*)$/; + +export default React.createClass({ + statics: { + isPillUrl: (url) => { + return !!REGEX_MATRIXTO.exec(url); + }, + isMessagePillUrl: (url) => { + return !!REGEX_LOCAL_MATRIXTO.exec(url); + }, + }, + + props: { + // The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl) + url: PropTypes.string, + // Whether the pill is in a message + inMessage: PropTypes.bool, + // The room in which this pill is being rendered + room: PropTypes.instanceOf(Room), + }, + + render: function() { + const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); + + let regex = REGEX_MATRIXTO; + if (this.props.inMessage) { + regex = REGEX_LOCAL_MATRIXTO; + } + + // Default to the empty array if no match for simplicity + // resource and prefix will be undefined instead of throwing + const matrixToMatch = regex.exec(this.props.url) || []; + console.info(matrixToMatch); + + const resource = matrixToMatch[1]; // The room/user ID + const prefix = matrixToMatch[2]; // The first character of prefix + + // Default to the room/user ID + let linkText = resource; + + const isUserPill = prefix === '@'; + const isRoomPill = prefix === '#' || prefix === '!'; + + let avatar = null; + let userId; + if (isUserPill) { + // If this user is not a member of this room, default to the empty + // member. This could be improved by doing an async profile lookup. + const member = this.props.room.getMember(resource) || + new RoomMember(null, resource); + if (member) { + userId = member.userId; + linkText = member.name; + avatar = ; + } + } else if (isRoomPill) { + const room = prefix === '#' ? + MatrixClientPeg.get().getRooms().find((r) => { + return r.getAliases().includes(resource); + }) : MatrixClientPeg.get().getRoom(resource); + + if (room) { + linkText = (room ? getDisplayAliasForRoom(room) : null) || resource; + avatar = ; + } + } + + const classes = classNames({ + "mx_UserPill": isUserPill, + "mx_RoomPill": isRoomPill, + "mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId, + }); + + if ((isUserPill || isRoomPill) && avatar) { + return this.props.inMessage ? + + {avatar} + {linkText} + : + + {avatar} + {linkText} + ; + } else { + // Deliberately render nothing if the URL isn't recognised + return null; + } + }, +}); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index b901f98f19..6d4d01a196 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -170,56 +170,21 @@ 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; - let userId; - switch (resourceType) { - case "user": - roomId = this.props.mxEvent.getRoomId(); - room = MatrixClientPeg.get().getRoom(roomId); - userId = resourceId; - member = room.getMember(userId) || - new RoomMember(null, userId); - 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 = classNames( - "mx_MTextBody_pill", - { - "mx_UserPill": match[1] === "user", - "mx_RoomPill": match[1] === "room", - "mx_UserPill_me": - userId === MatrixClientPeg.get().credentials.userId, - }, - ); - ReactDOM.render(avatar, avatarContainer); - node.insertBefore(avatarContainer, node.firstChild); - } + + // If the link is a (localised) matrix.to link, replace it with a pill + const Pill = sdk.getComponent('elements.Pill'); + if (Pill.isMessagePillUrl(href)) { + const pillContainer = document.createElement('span'); + + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const pill = ; + + ReactDOM.render(pill, pillContainer); + node.parentNode.replaceChild(pillContainer, node); } } else if (node.children && node.children.length) { this.pillifyLinks(node.children); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 2c011f3770..006aebff60 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -26,7 +26,6 @@ import Promise from 'bluebird'; import MatrixClientPeg from '../../../MatrixClientPeg'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; -import {RoomMember} from 'matrix-js-sdk'; import SlashCommands from '../../../SlashCommands'; import KeyCode from '../../../KeyCode'; import Modal from '../../../Modal'; @@ -43,10 +42,6 @@ import {Completion} from "../../../autocomplete/Autocompleter"; import Markdown from '../../../Markdown'; import ComposerHistoryManager from '../../../ComposerHistoryManager'; import MessageComposerStore from '../../../stores/MessageComposerStore'; -import { getDisplayAliasForRoom } from '../../../Rooms'; - -import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; -const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione'; const EMOJI_SHORTNAMES = Object.keys(emojioneList); @@ -188,56 +183,10 @@ export default class MessageComposerInput extends React.Component { decorators.push({ strategy: this.findLinkEntities.bind(this), component: (props) => { - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); + const Pill = sdk.getComponent('elements.Pill'); const {url} = Entity.get(props.entityKey).getData(); - - // Default to the empty array if no match for simplicity - // resource and prefix will be undefined instead of throwing - const matrixToMatch = REGEX_MATRIXTO.exec(url) || []; - - const resource = matrixToMatch[1]; // The room/user ID - const prefix = matrixToMatch[2]; // The first character of prefix - - // Default to the room/user ID - let linkText = resource; - - const isUserPill = prefix === '@'; - const isRoomPill = prefix === '#' || prefix === '!'; - - const classes = classNames({ - "mx_UserPill": isUserPill, - "mx_RoomPill": isRoomPill, - }); - - let avatar = null; - if (isUserPill) { - // If this user is not a member of this room, default to the empty - // member. This could be improved by doing an async profile lookup. - const member = this.props.room.getMember(resource) || - new RoomMember(null, resource); - - linkText = member.name; - - avatar = member ? : null; - } else if (isRoomPill) { - const room = prefix === '#' ? - MatrixClientPeg.get().getRooms().find((r) => { - return r.getCanonicalAlias() === resource; - }) : MatrixClientPeg.get().getRoom(resource); - - linkText = getDisplayAliasForRoom(room) || resource; - - avatar = room ? : null; - } - - if (isUserPill || isRoomPill) { - return ( - - {avatar} - {linkText} - - ); + if (Pill.isPillUrl(url)) { + return ; } return (