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
This commit is contained in:
Luke Barnard 2017-07-21 13:41:23 +01:00
parent 8c531a85e9
commit 7db7192701
3 changed files with 132 additions and 100 deletions

View File

@ -0,0 +1,118 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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);
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links
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
// TODO: 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) { pillifyLinks: function(nodes) {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; const node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href")) { if (node.tagName === "A" && node.getAttribute("href")) {
const href = node.getAttribute("href"); const href = node.getAttribute("href");
// HtmlUtils transforms `matrix.to` links to local links, so match against
// user or room app links. // If the link is a (localised) matrix.to link, replace it with a pill
const match = /^#\/(user|room)\/(.*)$/.exec(href) || []; const Pill = sdk.getComponent('elements.Pill');
const resourceType = match[1]; // "user" or "room" if (Pill.isMessagePillUrl(href)) {
const resourceId = match[2]; // user ID or room ID const pillContainer = document.createElement('span');
if (match && resourceType && resourceId) {
let avatar; const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
let roomId; const pill = <Pill url={href} inMessage={true} room={room}/>;
let room;
let member; ReactDOM.render(pill, pillContainer);
let userId; node.parentNode.replaceChild(pillContainer, node);
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);
}
} }
} else if (node.children && node.children.length) { } else if (node.children && node.children.length) {
this.pillifyLinks(node.children); this.pillifyLinks(node.children);

View File

@ -26,7 +26,6 @@ import Promise from 'bluebird';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import {RoomMember} from 'matrix-js-sdk';
import SlashCommands from '../../../SlashCommands'; import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode'; import KeyCode from '../../../KeyCode';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -43,10 +42,6 @@ import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown'; import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager'; import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore'; 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'; import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
const EMOJI_SHORTNAMES = Object.keys(emojioneList); const EMOJI_SHORTNAMES = Object.keys(emojioneList);
@ -188,56 +183,10 @@ export default class MessageComposerInput extends React.Component {
decorators.push({ decorators.push({
strategy: this.findLinkEntities.bind(this), strategy: this.findLinkEntities.bind(this),
component: (props) => { component: (props) => {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const Pill = sdk.getComponent('elements.Pill');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
const {url} = Entity.get(props.entityKey).getData(); const {url} = Entity.get(props.entityKey).getData();
if (Pill.isPillUrl(url)) {
// Default to the empty array if no match for simplicity return <Pill url={url} room={this.props.room}/>;
// 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>
);
} }
return ( return (