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
pull/21833/head
Luke Barnard 2017-07-21 13:41:23 +01:00
parent 8c531a85e9
commit 8ef820054d
3 changed files with 115 additions and 100 deletions

View File

@ -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 = <MemberAvatar member={member} width={16} height={16}/>;
}
} 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 = <RoomAvatar room={room} width={16} height={16}/>;
}
}
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 ?
<a className={classes} href={this.props.url}>
{avatar}
{linkText}
</a> :
<span className={classes}>
{avatar}
{linkText}
</span>;
} else {
// Deliberately render nothing if the URL isn't recognised
return null;
}
},
});

View File

@ -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 = <MemberAvatar member={member} width={16} height={16} name={userId}/>;
break;
case "room":
room = resourceId[0] === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
return r.getCanonicalAlias() === resourceId;
}) : MatrixClientPeg.get().getRoom(resourceId);
if (room) {
avatar = <RoomAvatar room={room} width={16} height={16}/>;
}
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 = <Pill url={href} inMessage={true} room={room}/>;
ReactDOM.render(pill, pillContainer);
node.parentNode.replaceChild(pillContainer, node);
}
} else if (node.children && node.children.length) {
this.pillifyLinks(node.children);

View File

@ -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 ? <MemberAvatar member={member} width={16} height={16}/> : 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 ? <RoomAvatar room={room} width={16} height={16}/> : null;
}
if (isUserPill || isRoomPill) {
return (
<span className={classes}>
{avatar}
{linkText}
</span>
);
if (Pill.isPillUrl(url)) {
return <Pill url={url} room={this.props.room}/>;
}
return (