2017-07-21 15:41:23 +03:00
|
|
|
/*
|
2021-03-06 04:49:32 +03:00
|
|
|
Copyright 2017 - 2019, 2021 The Matrix.org Foundation C.I.C.
|
2017-07-21 15:41:23 +03:00
|
|
|
|
|
|
|
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';
|
2019-12-20 04:19:56 +03:00
|
|
|
import * as sdk from '../../../index';
|
2020-05-14 05:41:41 +03:00
|
|
|
import dis from '../../../dispatcher/dispatcher';
|
2017-07-21 15:41:23 +03:00
|
|
|
import classNames from 'classnames';
|
2021-03-19 05:50:34 +03:00
|
|
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
|
|
|
import { RoomMember } from 'matrix-js-sdk/src/models/room-member'
|
2017-07-21 15:41:23 +03:00
|
|
|
import PropTypes from 'prop-types';
|
2019-12-21 00:13:46 +03:00
|
|
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
2018-06-19 13:53:17 +03:00
|
|
|
import FlairStore from "../../../stores/FlairStore";
|
2021-01-27 14:46:20 +03:00
|
|
|
import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
|
2019-12-17 20:26:12 +03:00
|
|
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
2020-05-14 06:03:12 +03:00
|
|
|
import {Action} from "../../../dispatcher/actions";
|
2021-03-04 05:06:46 +03:00
|
|
|
import {mediaFromMxc} from "../../../customisations/Media";
|
2021-02-12 10:18:27 +03:00
|
|
|
import Tooltip from './Tooltip';
|
2021-03-09 05:59:41 +03:00
|
|
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
2017-07-21 15:41:23 +03:00
|
|
|
|
2021-03-09 05:59:41 +03:00
|
|
|
@replaceableComponent("views.elements.Pill")
|
2020-08-29 14:14:16 +03:00
|
|
|
class Pill extends React.Component {
|
|
|
|
static roomNotifPos(text) {
|
|
|
|
return text.indexOf("@room");
|
|
|
|
}
|
|
|
|
|
|
|
|
static roomNotifLen() {
|
|
|
|
return "@room".length;
|
|
|
|
}
|
|
|
|
|
|
|
|
static TYPE_USER_MENTION = 'TYPE_USER_MENTION';
|
|
|
|
static TYPE_ROOM_MENTION = 'TYPE_ROOM_MENTION';
|
|
|
|
static TYPE_GROUP_MENTION = 'TYPE_GROUP_MENTION';
|
|
|
|
static TYPE_AT_ROOM_MENTION = 'TYPE_AT_ROOM_MENTION'; // '@room' mention
|
|
|
|
|
|
|
|
static propTypes = {
|
2017-11-01 22:42:47 +03:00
|
|
|
// The Type of this Pill. If url is given, this is auto-detected.
|
|
|
|
type: PropTypes.string,
|
2021-01-26 17:11:09 +03:00
|
|
|
// The URL to pillify (no validation is done)
|
2017-07-21 15:41:23 +03:00
|
|
|
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),
|
2017-08-08 13:13:29 +03:00
|
|
|
// Whether to include an avatar in the pill
|
|
|
|
shouldShowPillAvatar: PropTypes.bool,
|
2018-05-14 05:02:12 +03:00
|
|
|
// Whether to render this pill as if it were highlit by a selection
|
|
|
|
isSelected: PropTypes.bool,
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2017-07-21 15:41:23 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
state = {
|
|
|
|
// ID/alias of the room/user
|
|
|
|
resourceId: null,
|
|
|
|
// Type of pill
|
|
|
|
pillType: null,
|
2017-07-24 19:18:29 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
// The member related to the user pill
|
|
|
|
member: null,
|
|
|
|
// The group related to the group pill
|
|
|
|
group: null,
|
|
|
|
// The room related to the room pill
|
|
|
|
room: null,
|
2021-02-12 10:18:27 +03:00
|
|
|
// Is the user hovering the pill
|
|
|
|
hover: false,
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2017-07-21 15:41:23 +03:00
|
|
|
|
2020-04-01 23:35:39 +03:00
|
|
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
2020-08-29 14:57:11 +03:00
|
|
|
// eslint-disable-next-line camelcase
|
2020-04-01 23:49:39 +03:00
|
|
|
async UNSAFE_componentWillReceiveProps(nextProps) {
|
2017-11-01 22:42:47 +03:00
|
|
|
let resourceId;
|
|
|
|
let prefix;
|
|
|
|
|
|
|
|
if (nextProps.url) {
|
2019-10-01 05:27:51 +03:00
|
|
|
if (nextProps.inMessage) {
|
2021-01-27 14:46:20 +03:00
|
|
|
const parts = parseAppLocalLink(nextProps.url);
|
|
|
|
resourceId = parts.primaryEntityId; // The room/user ID
|
|
|
|
prefix = parts.sigil; // The first character of prefix
|
2019-10-01 05:27:51 +03:00
|
|
|
} else {
|
|
|
|
resourceId = getPrimaryPermalinkEntity(nextProps.url);
|
|
|
|
prefix = resourceId ? resourceId[0] : undefined;
|
|
|
|
}
|
2017-11-01 22:42:47 +03:00
|
|
|
}
|
2017-07-21 15:41:23 +03:00
|
|
|
|
2017-11-01 22:42:47 +03:00
|
|
|
const pillType = this.props.type || {
|
2017-07-24 19:18:29 +03:00
|
|
|
'@': Pill.TYPE_USER_MENTION,
|
|
|
|
'#': Pill.TYPE_ROOM_MENTION,
|
|
|
|
'!': Pill.TYPE_ROOM_MENTION,
|
2018-06-19 13:53:17 +03:00
|
|
|
'+': Pill.TYPE_GROUP_MENTION,
|
2017-07-24 19:18:29 +03:00
|
|
|
}[prefix];
|
|
|
|
|
|
|
|
let member;
|
2018-06-19 13:53:17 +03:00
|
|
|
let group;
|
2017-07-24 19:18:29 +03:00
|
|
|
let room;
|
|
|
|
switch (pillType) {
|
2017-11-01 22:42:47 +03:00
|
|
|
case Pill.TYPE_AT_ROOM_MENTION: {
|
|
|
|
room = nextProps.room;
|
|
|
|
}
|
|
|
|
break;
|
2017-07-24 19:18:29 +03:00
|
|
|
case Pill.TYPE_USER_MENTION: {
|
2019-12-17 19:54:59 +03:00
|
|
|
const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined;
|
2017-07-24 19:18:29 +03:00
|
|
|
member = localMember;
|
|
|
|
if (!localMember) {
|
|
|
|
member = new RoomMember(null, resourceId);
|
|
|
|
this.doProfileLookup(resourceId, member);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case Pill.TYPE_ROOM_MENTION: {
|
|
|
|
const localRoom = resourceId[0] === '#' ?
|
|
|
|
MatrixClientPeg.get().getRooms().find((r) => {
|
2020-02-20 19:43:33 +03:00
|
|
|
return r.getCanonicalAlias() === resourceId ||
|
2020-02-21 15:00:09 +03:00
|
|
|
r.getAltAliases().includes(resourceId);
|
2017-07-24 19:18:29 +03:00
|
|
|
}) : MatrixClientPeg.get().getRoom(resourceId);
|
|
|
|
room = localRoom;
|
|
|
|
if (!localRoom) {
|
|
|
|
// TODO: This would require a new API to resolve a room alias to
|
|
|
|
// a room avatar and name.
|
|
|
|
// this.doRoomProfileLookup(resourceId, member);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2018-06-19 13:53:17 +03:00
|
|
|
case Pill.TYPE_GROUP_MENTION: {
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
|
|
|
try {
|
|
|
|
group = await FlairStore.getGroupProfileCached(cli, resourceId);
|
2018-06-22 14:11:16 +03:00
|
|
|
} catch (e) { // if FlairStore failed, fall back to just groupId
|
|
|
|
group = {
|
|
|
|
groupId: resourceId,
|
|
|
|
avatarUrl: null,
|
|
|
|
name: null,
|
|
|
|
};
|
2018-06-19 13:53:17 +03:00
|
|
|
}
|
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
}
|
2018-06-19 13:53:17 +03:00
|
|
|
this.setState({resourceId, pillType, member, group, room});
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2017-07-21 15:41:23 +03:00
|
|
|
|
2020-03-31 23:14:17 +03:00
|
|
|
componentDidMount() {
|
2017-08-01 18:20:32 +03:00
|
|
|
this._unmounted = false;
|
2018-02-06 20:50:53 +03:00
|
|
|
this._matrixClient = MatrixClientPeg.get();
|
2020-04-01 23:49:39 +03:00
|
|
|
|
|
|
|
// eslint-disable-next-line new-cap
|
2020-04-01 23:35:39 +03:00
|
|
|
this.UNSAFE_componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves.
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2017-08-01 18:20:32 +03:00
|
|
|
|
2017-07-25 11:22:08 +03:00
|
|
|
componentWillUnmount() {
|
|
|
|
this._unmounted = true;
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2017-07-25 11:22:08 +03:00
|
|
|
|
2021-02-12 10:18:27 +03:00
|
|
|
onMouseOver = () => {
|
|
|
|
this.setState({
|
|
|
|
hover: true,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
onMouseLeave = () => {
|
|
|
|
this.setState({
|
|
|
|
hover: false,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
doProfileLookup(userId, member) {
|
2017-07-25 11:20:14 +03:00
|
|
|
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
|
2017-07-25 11:22:08 +03:00
|
|
|
if (this._unmounted) {
|
|
|
|
return;
|
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
member.name = resp.displayname;
|
|
|
|
member.rawDisplayName = resp.displayname;
|
|
|
|
member.events.member = {
|
|
|
|
getContent: () => {
|
|
|
|
return {avatar_url: resp.avatar_url};
|
|
|
|
},
|
2018-08-02 18:09:18 +03:00
|
|
|
getDirectionalContent: function() {
|
|
|
|
return this.getContent();
|
2018-08-02 20:57:20 +03:00
|
|
|
},
|
2017-07-24 19:18:29 +03:00
|
|
|
};
|
|
|
|
this.setState({member});
|
2017-07-25 11:20:14 +03:00
|
|
|
}).catch((err) => {
|
|
|
|
console.error('Could not retrieve profile data for ' + userId + ':', err);
|
2017-07-24 19:18:29 +03:00
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
onUserPillClicked = () => {
|
2017-08-14 16:44:08 +03:00
|
|
|
dis.dispatch({
|
2020-05-14 06:03:12 +03:00
|
|
|
action: Action.ViewUser,
|
2017-08-14 16:44:08 +03:00
|
|
|
member: this.state.member,
|
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
render() {
|
2018-06-19 13:53:17 +03:00
|
|
|
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
2017-07-24 19:18:29 +03:00
|
|
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
|
|
|
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
|
|
|
|
|
|
|
const resource = this.state.resourceId;
|
2017-07-21 15:41:23 +03:00
|
|
|
|
|
|
|
let avatar = null;
|
2017-07-24 19:18:29 +03:00
|
|
|
let linkText = resource;
|
|
|
|
let pillClass;
|
2017-07-21 15:41:23 +03:00
|
|
|
let userId;
|
2017-08-14 16:44:08 +03:00
|
|
|
let href = this.props.url;
|
|
|
|
let onClick;
|
2017-07-24 19:18:29 +03:00
|
|
|
switch (this.state.pillType) {
|
2017-11-01 22:42:47 +03:00
|
|
|
case Pill.TYPE_AT_ROOM_MENTION: {
|
|
|
|
const room = this.props.room;
|
|
|
|
if (room) {
|
|
|
|
linkText = "@room";
|
|
|
|
if (this.props.shouldShowPillAvatar) {
|
2020-02-12 12:54:08 +03:00
|
|
|
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
|
2017-11-01 22:42:47 +03:00
|
|
|
}
|
|
|
|
pillClass = 'mx_AtRoomPill';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2017-07-24 19:18:29 +03:00
|
|
|
case Pill.TYPE_USER_MENTION: {
|
|
|
|
// If this user is not a member of this room, default to the empty member
|
|
|
|
const member = this.state.member;
|
|
|
|
if (member) {
|
|
|
|
userId = member.userId;
|
2018-04-10 00:59:46 +03:00
|
|
|
member.rawDisplayName = member.rawDisplayName || '';
|
2018-10-04 12:34:34 +03:00
|
|
|
linkText = member.rawDisplayName;
|
2017-08-08 13:13:29 +03:00
|
|
|
if (this.props.shouldShowPillAvatar) {
|
2020-02-12 12:54:08 +03:00
|
|
|
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />;
|
2017-08-08 13:13:29 +03:00
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
pillClass = 'mx_UserPill';
|
2017-08-14 16:44:08 +03:00
|
|
|
href = null;
|
2017-08-18 19:15:27 +03:00
|
|
|
onClick = this.onUserPillClicked;
|
2017-07-24 19:18:29 +03:00
|
|
|
}
|
2017-07-21 15:41:23 +03:00
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
break;
|
|
|
|
case Pill.TYPE_ROOM_MENTION: {
|
|
|
|
const room = this.state.room;
|
|
|
|
if (room) {
|
2021-02-06 21:09:34 +03:00
|
|
|
linkText = room.name || resource;
|
2017-08-08 13:13:29 +03:00
|
|
|
if (this.props.shouldShowPillAvatar) {
|
2020-02-12 12:54:08 +03:00
|
|
|
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
|
2017-08-08 13:13:29 +03:00
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
}
|
2020-02-21 15:00:32 +03:00
|
|
|
pillClass = 'mx_RoomPill';
|
2017-07-21 15:41:23 +03:00
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
break;
|
2018-06-19 13:53:17 +03:00
|
|
|
case Pill.TYPE_GROUP_MENTION: {
|
|
|
|
if (this.state.group) {
|
|
|
|
const {avatarUrl, groupId, name} = this.state.group;
|
|
|
|
|
|
|
|
linkText = groupId;
|
|
|
|
if (this.props.shouldShowPillAvatar) {
|
2021-03-06 04:49:32 +03:00
|
|
|
avatar = <BaseAvatar
|
|
|
|
name={name || groupId} width={16} height={16} aria-hidden="true"
|
|
|
|
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
|
2018-06-19 13:53:17 +03:00
|
|
|
}
|
2018-06-22 16:36:29 +03:00
|
|
|
pillClass = 'mx_GroupPill';
|
2018-06-19 13:53:17 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2017-07-21 15:41:23 +03:00
|
|
|
}
|
|
|
|
|
2019-01-07 18:13:01 +03:00
|
|
|
const classes = classNames("mx_Pill", pillClass, {
|
2020-01-06 16:28:29 +03:00
|
|
|
"mx_UserPill_me": userId === MatrixClientPeg.get().getUserId(),
|
2018-05-14 05:02:12 +03:00
|
|
|
"mx_UserPill_selected": this.props.isSelected,
|
2017-07-21 15:41:23 +03:00
|
|
|
});
|
|
|
|
|
2017-07-24 19:18:29 +03:00
|
|
|
if (this.state.pillType) {
|
2021-02-12 10:18:27 +03:00
|
|
|
const {yOffset} = this.props;
|
|
|
|
|
|
|
|
let tip;
|
2021-02-12 16:28:39 +03:00
|
|
|
if (this.state.hover && resource) {
|
2021-02-12 10:18:27 +03:00
|
|
|
tip = <Tooltip label={resource} yOffset={yOffset} />;
|
|
|
|
}
|
|
|
|
|
2019-12-17 20:26:12 +03:00
|
|
|
return <MatrixClientContext.Provider value={this._matrixClient}>
|
|
|
|
{ this.props.inMessage ?
|
2021-02-12 10:18:27 +03:00
|
|
|
<a
|
|
|
|
className={classes}
|
|
|
|
href={href}
|
|
|
|
onClick={onClick}
|
|
|
|
data-offset-key={this.props.offsetKey}
|
|
|
|
onMouseOver={this.onMouseOver}
|
|
|
|
onMouseLeave={this.onMouseLeave}
|
|
|
|
>
|
2019-12-17 20:26:12 +03:00
|
|
|
{ avatar }
|
|
|
|
{ linkText }
|
2021-02-12 13:27:14 +03:00
|
|
|
{ tip }
|
2019-12-17 20:26:12 +03:00
|
|
|
</a> :
|
2021-02-12 10:18:27 +03:00
|
|
|
<span
|
|
|
|
className={classes}
|
|
|
|
data-offset-key={this.props.offsetKey}
|
|
|
|
onMouseOver={this.onMouseOver}
|
|
|
|
onMouseLeave={this.onMouseLeave}
|
|
|
|
>
|
2019-12-17 20:26:12 +03:00
|
|
|
{ avatar }
|
|
|
|
{ linkText }
|
2021-02-12 13:27:14 +03:00
|
|
|
{ tip }
|
2019-12-17 20:26:12 +03:00
|
|
|
</span> }
|
|
|
|
</MatrixClientContext.Provider>;
|
2017-07-21 15:41:23 +03:00
|
|
|
} else {
|
|
|
|
// Deliberately render nothing if the URL isn't recognised
|
|
|
|
return null;
|
|
|
|
}
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
|
|
|
}
|
2017-07-24 19:18:29 +03:00
|
|
|
|
|
|
|
export default Pill;
|