Merge pull request #1241 from matrix-org/luke/refactor-pills
Factor out shared logic in two code-paths for pill rendering
This commit is contained in:
commit
eb0575d9c1
3 changed files with 131 additions and 100 deletions
117
src/components/views/elements/Pill.js
Normal file
117
src/components/views/elements/Pill.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
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) || [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -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);
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
Loading…
Reference in a new issue