Merge branch 'develop' into luke/store-history-as-raw-content
This commit is contained in:
commit
fc00eaf546
12 changed files with 554 additions and 176 deletions
|
@ -34,12 +34,9 @@ export type Completion = {
|
||||||
component: ?Component,
|
component: ?Component,
|
||||||
range: SelectionRange,
|
range: SelectionRange,
|
||||||
command: ?string,
|
command: ?string,
|
||||||
// An entity applied during the replacement (using draftjs@0.8.1 Entity.create)
|
// If provided, apply a LINK entity to the completion with the
|
||||||
entity: ? {
|
// data = { url: href }.
|
||||||
type: string,
|
href: ?string,
|
||||||
mutability: string,
|
|
||||||
data: ?Object,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
|
|
|
@ -22,6 +22,8 @@ import dis from '../../dispatcher';
|
||||||
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
|
import Modal from '../../Modal';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
const RoomSummaryType = PropTypes.shape({
|
const RoomSummaryType = PropTypes.shape({
|
||||||
room_id: PropTypes.string.isRequired,
|
room_id: PropTypes.string.isRequired,
|
||||||
|
@ -179,10 +181,13 @@ export default React.createClass({
|
||||||
summary: null,
|
summary: null,
|
||||||
error: null,
|
error: null,
|
||||||
editing: false,
|
editing: false,
|
||||||
|
saving: false,
|
||||||
|
uploadingAvatar: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._changeAvatarComponent = null;
|
||||||
this._loadGroupFromServer(this.props.groupId);
|
this._loadGroupFromServer(this.props.groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -211,8 +216,83 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onSettingsClick: function() {
|
_onEditClick: function() {
|
||||||
this.setState({editing: true});
|
this.setState({
|
||||||
|
editing: true,
|
||||||
|
profileForm: Object.assign({}, this.state.summary.profile),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCancelClick: function() {
|
||||||
|
this.setState({
|
||||||
|
editing: false,
|
||||||
|
profileForm: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onNameChange: function(e) {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { name: e.target.value });
|
||||||
|
this.setState({
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onShortDescChange: function(e) {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { short_description: e.target.value });
|
||||||
|
this.setState({
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onLongDescChange: function(e) {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { long_description: e.target.value });
|
||||||
|
this.setState({
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAvatarSelected: function(ev) {
|
||||||
|
const file = ev.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
this.setState({uploadingAvatar: true});
|
||||||
|
MatrixClientPeg.get().uploadContent(file).then((url) => {
|
||||||
|
const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
|
||||||
|
this.setState({
|
||||||
|
uploadingAvatar: false,
|
||||||
|
profileForm: newProfileForm,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({uploadingAvatar: false});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to upload avatar image", e);
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t('Error'),
|
||||||
|
description: _t('Failed to upload image'),
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSaveClick: function() {
|
||||||
|
this.setState({saving: true});
|
||||||
|
MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm).then((result) => {
|
||||||
|
this.setState({
|
||||||
|
saving: false,
|
||||||
|
editing: false,
|
||||||
|
summary: null,
|
||||||
|
});
|
||||||
|
this._loadGroupFromServer(this.props.groupId);
|
||||||
|
}).catch((e) => {
|
||||||
|
this.setState({
|
||||||
|
saving: false,
|
||||||
|
});
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to save group profile", e);
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t('Error'),
|
||||||
|
description: _t('Failed to update group'),
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getFeaturedRoomsNode() {
|
_getFeaturedRoomsNode() {
|
||||||
|
@ -296,60 +376,129 @@ export default React.createClass({
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
if (this.state.summary === null && this.state.error === null) {
|
if (this.state.summary === null && this.state.error === null || this.state.saving) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
} else if (this.state.editing) {
|
|
||||||
return <div />;
|
|
||||||
} else if (this.state.summary) {
|
} else if (this.state.summary) {
|
||||||
const summary = this.state.summary;
|
const summary = this.state.summary;
|
||||||
let description = null;
|
|
||||||
if (summary.profile && summary.profile.long_description) {
|
let avatarNode;
|
||||||
description = sanitizedHtmlNode(summary.profile.long_description);
|
let nameNode;
|
||||||
|
let shortDescNode;
|
||||||
|
let rightButtons;
|
||||||
|
let roomBody;
|
||||||
|
const headerClasses = {
|
||||||
|
mx_GroupView_header: true,
|
||||||
|
};
|
||||||
|
if (this.state.editing) {
|
||||||
|
let avatarImage;
|
||||||
|
if (this.state.uploadingAvatar) {
|
||||||
|
avatarImage = <Loader />;
|
||||||
|
} else {
|
||||||
|
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
|
||||||
|
avatarImage = <GroupAvatar groupId={this.props.groupId}
|
||||||
|
groupAvatarUrl={this.state.profileForm.avatar_url}
|
||||||
|
width={48} height={48} resizeMethod='crop'
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomBody = <div>
|
avatarNode = (
|
||||||
<div className="mx_GroupView_groupDesc">{description}</div>
|
<div className="mx_GroupView_avatarPicker">
|
||||||
{this._getFeaturedRoomsNode()}
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
{this._getFeaturedUsersNode()}
|
{avatarImage}
|
||||||
|
</label>
|
||||||
|
<div className="mx_GroupView_avatarPicker_edit">
|
||||||
|
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||||
|
<img src="img/camera.svg"
|
||||||
|
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
||||||
|
width="17" height="15" />
|
||||||
|
</label>
|
||||||
|
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
nameNode = <input type="text"
|
||||||
|
value={this.state.profileForm.name}
|
||||||
|
onChange={this._onNameChange}
|
||||||
|
placeholder={_t('Group Name')}
|
||||||
|
tabIndex="1"
|
||||||
|
/>;
|
||||||
|
shortDescNode = <input type="text"
|
||||||
|
value={this.state.profileForm.short_description}
|
||||||
|
onChange={this._onShortDescChange}
|
||||||
|
placeholder={_t('Description')}
|
||||||
|
tabIndex="2"
|
||||||
|
/>;
|
||||||
|
rightButtons = <span>
|
||||||
|
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
|
||||||
|
{_t('Save')}
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
|
||||||
|
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
||||||
|
width="18" height="18" alt={_t("Cancel")}/>
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>;
|
||||||
|
roomBody = <div>
|
||||||
|
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
|
||||||
|
onChange={this._onLongDescChange}
|
||||||
|
tabIndex="3"
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
|
} else {
|
||||||
let nameNode;
|
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||||
|
avatarNode = <GroupAvatar
|
||||||
|
groupId={this.props.groupId}
|
||||||
|
groupAvatarUrl={groupAvatarUrl}
|
||||||
|
width={48} height={48}
|
||||||
|
/>;
|
||||||
if (summary.profile && summary.profile.name) {
|
if (summary.profile && summary.profile.name) {
|
||||||
nameNode = <div className="mx_RoomHeader_name">
|
nameNode = <div>
|
||||||
<span>{summary.profile.name}</span>
|
<span>{summary.profile.name}</span>
|
||||||
<span className="mx_GroupView_header_groupid">
|
<span className="mx_GroupView_header_groupid">
|
||||||
({this.props.groupId})
|
({this.props.groupId})
|
||||||
</span>
|
</span>
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
nameNode = <div className="mx_RoomHeader_name">
|
nameNode = <span>{this.props.groupId}</span>;
|
||||||
<span>{this.props.groupId}</span>
|
}
|
||||||
|
shortDescNode = <span>{summary.profile.short_description}</span>;
|
||||||
|
|
||||||
|
let description = null;
|
||||||
|
if (summary.profile && summary.profile.long_description) {
|
||||||
|
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||||
|
}
|
||||||
|
roomBody = <div>
|
||||||
|
<div className="mx_GroupView_groupDesc">{description}</div>
|
||||||
|
{this._getFeaturedRoomsNode()}
|
||||||
|
{this._getFeaturedUsersNode()}
|
||||||
</div>;
|
</div>;
|
||||||
|
// disabled until editing works
|
||||||
|
rightButtons = <AccessibleButton className="mx_GroupHeader_button"
|
||||||
|
onClick={this._onEditClick} title={_t("Edit Group")}
|
||||||
|
>
|
||||||
|
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||||
|
</AccessibleButton>;
|
||||||
|
|
||||||
|
headerClasses.mx_GroupView_header_view = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
|
||||||
|
|
||||||
// settings button is display: none until settings is wired up
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_GroupView">
|
<div className="mx_GroupView">
|
||||||
<div className="mx_RoomHeader">
|
<div className={classnames(headerClasses)}>
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_GroupView_header_leftCol">
|
||||||
<div className="mx_RoomHeader_avatar">
|
<div className="mx_GroupView_header_avatar">
|
||||||
<GroupAvatar
|
{avatarNode}
|
||||||
groupId={this.props.groupId}
|
|
||||||
groupAvatarUrl={groupAvatarUrl}
|
|
||||||
width={48} height={48}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomHeader_info">
|
<div className="mx_GroupView_header_info">
|
||||||
|
<div className="mx_GroupView_header_name">
|
||||||
{nameNode}
|
{nameNode}
|
||||||
<div className="mx_RoomHeader_topic">
|
</div>
|
||||||
{summary.profile.short_description}
|
<div className="mx_GroupView_header_shortDesc">
|
||||||
|
{shortDescNode}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this._onSettingsClick} title={_t("Settings")} style={{display: 'none'}}>
|
</div>
|
||||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
<div className="mx_GroupView_header_rightCol">
|
||||||
</AccessibleButton>
|
{rightButtons}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{roomBody}
|
{roomBody}
|
||||||
|
|
|
@ -307,6 +307,7 @@ export default React.createClass({
|
||||||
page_element = <GroupView
|
page_element = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
/>;
|
/>;
|
||||||
|
//right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ export default React.createClass({
|
||||||
loading: false,
|
loading: false,
|
||||||
widgetUrl: this.props.url,
|
widgetUrl: this.props.url,
|
||||||
error: null,
|
error: null,
|
||||||
|
deleting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -101,6 +102,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_onDeleteClick: function() {
|
_onDeleteClick: function() {
|
||||||
console.log("Delete widget %s", this.props.id);
|
console.log("Delete widget %s", this.props.id);
|
||||||
|
this.setState({deleting: true});
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
'im.vector.modular.widgets',
|
'im.vector.modular.widgets',
|
||||||
|
@ -110,6 +112,7 @@ export default React.createClass({
|
||||||
console.log('Deleted widget');
|
console.log('Deleted widget');
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
console.error('Failed to delete widget', e);
|
console.error('Failed to delete widget', e);
|
||||||
|
this.setState({deleting: false});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -124,6 +127,12 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
|
// Don't render widget if it is in the process of being deleted
|
||||||
|
if (this.state.deleting) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div> Loading... </div>
|
<div> Loading... </div>
|
||||||
|
|
191
src/components/views/elements/Pill.js
Normal file
191
src/components/views/elements/Pill.js
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
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)\/(([\#\!\@\+]).*)$/;
|
||||||
|
|
||||||
|
const Pill = React.createClass({
|
||||||
|
statics: {
|
||||||
|
isPillUrl: (url) => {
|
||||||
|
return !!REGEX_MATRIXTO.exec(url);
|
||||||
|
},
|
||||||
|
isMessagePillUrl: (url) => {
|
||||||
|
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
||||||
|
},
|
||||||
|
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||||
|
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||||
|
},
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState() {
|
||||||
|
return {
|
||||||
|
// ID/alias of the room/user
|
||||||
|
resourceId: null,
|
||||||
|
// Type of pill
|
||||||
|
pillType: null,
|
||||||
|
|
||||||
|
// The member related to the user pill
|
||||||
|
member: null,
|
||||||
|
// The room related to the room pill
|
||||||
|
room: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._unmounted = false;
|
||||||
|
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 resourceId = matrixToMatch[1]; // The room/user ID
|
||||||
|
const prefix = matrixToMatch[2]; // The first character of prefix
|
||||||
|
|
||||||
|
const pillType = {
|
||||||
|
'@': Pill.TYPE_USER_MENTION,
|
||||||
|
'#': Pill.TYPE_ROOM_MENTION,
|
||||||
|
'!': Pill.TYPE_ROOM_MENTION,
|
||||||
|
}[prefix];
|
||||||
|
|
||||||
|
let member;
|
||||||
|
let room;
|
||||||
|
switch (pillType) {
|
||||||
|
case Pill.TYPE_USER_MENTION: {
|
||||||
|
const localMember = this.props.room.getMember(resourceId);
|
||||||
|
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) => {
|
||||||
|
return r.getAliases().includes(resourceId);
|
||||||
|
}) : 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;
|
||||||
|
}
|
||||||
|
this.setState({resourceId, pillType, member, room});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
doProfileLookup: function(userId, member) {
|
||||||
|
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
member.name = resp.displayname;
|
||||||
|
member.rawDisplayName = resp.displayname;
|
||||||
|
member.events.member = {
|
||||||
|
getContent: () => {
|
||||||
|
return {avatar_url: resp.avatar_url};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.setState({member});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('Could not retrieve profile data for ' + userId + ':', err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||||
|
|
||||||
|
const resource = this.state.resourceId;
|
||||||
|
|
||||||
|
let avatar = null;
|
||||||
|
let linkText = resource;
|
||||||
|
let pillClass;
|
||||||
|
let userId;
|
||||||
|
switch (this.state.pillType) {
|
||||||
|
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;
|
||||||
|
linkText = member.name;
|
||||||
|
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
||||||
|
pillClass = 'mx_UserPill';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Pill.TYPE_ROOM_MENTION: {
|
||||||
|
const room = this.state.room;
|
||||||
|
if (room) {
|
||||||
|
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||||
|
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
||||||
|
pillClass = 'mx_RoomPill';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames(pillClass, {
|
||||||
|
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.pillType) {
|
||||||
|
return this.props.inMessage ?
|
||||||
|
<a className={classes} href={this.props.url} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
|
{avatar}
|
||||||
|
{linkText}
|
||||||
|
</a> :
|
||||||
|
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||||
|
{avatar}
|
||||||
|
{linkText}
|
||||||
|
</span>;
|
||||||
|
} else {
|
||||||
|
// Deliberately render nothing if the URL isn't recognised
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Pill;
|
|
@ -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);
|
||||||
|
@ -297,10 +262,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onEmoteSenderClick: function(event) {
|
onEmoteSenderClick: function(event) {
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'insert_displayname',
|
action: 'insert_mention',
|
||||||
displayname: name.replace(' (IRC)', ''),
|
user_id: mxEvent.getSender(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -357,10 +357,10 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onSenderProfileClick: function(event) {
|
onSenderProfileClick: function(event) {
|
||||||
var mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'insert_displayname',
|
action: 'insert_mention',
|
||||||
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
|
user_id: mxEvent.getSender(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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,10 @@ 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';
|
import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||||
|
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
||||||
|
|
||||||
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);
|
||||||
|
@ -187,62 +186,16 @@ export default class MessageComposerInput extends React.Component {
|
||||||
RichText.getScopedMDDecorators(this.props);
|
RichText.getScopedMDDecorators(this.props);
|
||||||
decorators.push({
|
decorators.push({
|
||||||
strategy: this.findLinkEntities.bind(this),
|
strategy: this.findLinkEntities.bind(this),
|
||||||
component: (props) => {
|
component: (entityProps) => {
|
||||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
const {url} = Entity.get(entityProps.entityKey).getData();
|
||||||
const {url} = Entity.get(props.entityKey).getData();
|
if (Pill.isPillUrl(url)) {
|
||||||
|
return <Pill url={url} room={this.props.room} offsetKey={entityProps.offsetKey}/>;
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={url}>
|
<a href={url} data-offset-key={entityProps.offsetKey}>
|
||||||
{props.children}
|
{entityProps.children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -285,22 +238,20 @@ export default class MessageComposerInput extends React.Component {
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
editor.focus();
|
editor.focus();
|
||||||
break;
|
break;
|
||||||
|
case 'insert_mention': {
|
||||||
// TODO change this so we insert a complete user alias
|
// Pretend that we've autocompleted this user because keeping two code
|
||||||
|
// paths for inserting a user pill is not fun
|
||||||
case 'insert_displayname': {
|
const selection = this.state.editorState.getSelection();
|
||||||
contentState = Modifier.replaceText(
|
const member = this.props.room.getMember(payload.user_id);
|
||||||
contentState,
|
const completion = member ? member.name.replace(' (IRC)', '') : payload.user_id;
|
||||||
this.state.editorState.getSelection(),
|
this.setDisplayedCompletion({
|
||||||
`${payload.displayname}: `,
|
completion,
|
||||||
);
|
selection,
|
||||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
href: `https://matrix.to/#/${payload.user_id}`,
|
||||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
suffix: selection.getStartOffset() === 0 ? ': ' : ' ',
|
||||||
this.onEditorContentChanged(editorState);
|
});
|
||||||
editor.focus();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'quote': {
|
case 'quote': {
|
||||||
let {body, formatted_body} = payload.event.getContent();
|
let {body, formatted_body} = payload.event.getContent();
|
||||||
formatted_body = formatted_body || escape(body);
|
formatted_body = formatted_body || escape(body);
|
||||||
|
@ -775,6 +726,35 @@ export default class MessageComposerInput extends React.Component {
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = this.client.sendEmoteMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip MD user (tab-completed) mentions to preserve plaintext mention behaviour
|
||||||
|
contentText = contentText.replace(REGEX_MATRIXTO_MARKDOWN_GLOBAL,
|
||||||
|
(markdownLink, text, resource, prefix, offset) => {
|
||||||
|
// Calculate the offset relative to the current block that the offset is in
|
||||||
|
let sum = 0;
|
||||||
|
const blocks = contentState.getBlocksAsArray();
|
||||||
|
let block;
|
||||||
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
block = blocks[i];
|
||||||
|
sum += block.getLength();
|
||||||
|
if (sum > offset) {
|
||||||
|
sum -= block.getLength();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset -= sum;
|
||||||
|
|
||||||
|
const entityKey = block.getEntityAt(offset);
|
||||||
|
const entity = entityKey ? Entity.get(entityKey) : null;
|
||||||
|
if (entity && entity.getData().isCompletion && prefix === '@') {
|
||||||
|
// This is a completed mention, so do not insert MD link, just text
|
||||||
|
return text;
|
||||||
|
} else {
|
||||||
|
// This is either a MD link that was typed into the composer or another
|
||||||
|
// type of pill (e.g. room pill)
|
||||||
|
return markdownLink;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let sendMessagePromise;
|
let sendMessagePromise;
|
||||||
if (contentHTML) {
|
if (contentHTML) {
|
||||||
sendMessagePromise = sendHtmlFn.call(
|
sendMessagePromise = sendHtmlFn.call(
|
||||||
|
@ -933,21 +913,32 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {range = {}, completion = '', href = null, suffix = ''} = displayedCompletion;
|
const {range = null, completion = '', href = null, suffix = ''} = displayedCompletion;
|
||||||
|
|
||||||
let entityKey;
|
let entityKey;
|
||||||
let mdCompletion;
|
let mdCompletion;
|
||||||
if (href) {
|
if (href) {
|
||||||
entityKey = Entity.create('LINK', 'IMMUTABLE', {url: href});
|
entityKey = Entity.create('LINK', 'IMMUTABLE', {
|
||||||
|
url: href,
|
||||||
|
isCompletion: true,
|
||||||
|
});
|
||||||
if (!this.state.isRichtextEnabled) {
|
if (!this.state.isRichtextEnabled) {
|
||||||
mdCompletion = `[${completion}](${href})`;
|
mdCompletion = `[${completion}](${href})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selection;
|
||||||
|
if (range) {
|
||||||
|
selection = RichText.textOffsetsToSelectionState(
|
||||||
|
range, activeEditorState.getCurrentContent().getBlocksAsArray(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selection = activeEditorState.getSelection();
|
||||||
|
}
|
||||||
|
|
||||||
let contentState = Modifier.replaceText(
|
let contentState = Modifier.replaceText(
|
||||||
activeEditorState.getCurrentContent(),
|
activeEditorState.getCurrentContent(),
|
||||||
RichText.textOffsetsToSelectionState(
|
selection,
|
||||||
range, activeEditorState.getCurrentContent().getBlocksAsArray(),
|
|
||||||
),
|
|
||||||
mdCompletion || completion,
|
mdCompletion || completion,
|
||||||
null,
|
null,
|
||||||
entityKey,
|
entityKey,
|
||||||
|
|
|
@ -956,5 +956,8 @@
|
||||||
"Featured Rooms:": "Featured Rooms:",
|
"Featured Rooms:": "Featured Rooms:",
|
||||||
"Error whilst fetching joined groups": "Error whilst fetching joined groups",
|
"Error whilst fetching joined groups": "Error whilst fetching joined groups",
|
||||||
"Featured Users:": "Featured Users:",
|
"Featured Users:": "Featured Users:",
|
||||||
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji"
|
"Edit Group": "Edit Group",
|
||||||
|
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
|
||||||
|
"Failed to upload image": "Failed to upload image",
|
||||||
|
"Failed to update group": "Failed to update group"
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,8 @@ matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
|
||||||
+ ")(#.*)";
|
+ ")(#.*)";
|
||||||
|
|
||||||
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";
|
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";
|
||||||
|
matrixLinkify.MATRIXTO_MD_LINK_PATTERN =
|
||||||
|
'\\[([^\\]]*)\\]\\((?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!)[^\\)]*)\\)';
|
||||||
matrixLinkify.MATRIXTO_BASE_URL= "https://matrix.to";
|
matrixLinkify.MATRIXTO_BASE_URL= "https://matrix.to";
|
||||||
|
|
||||||
matrixLinkify.options = {
|
matrixLinkify.options = {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import sdk from 'matrix-react-sdk';
|
||||||
import UserSettingsStore from '../../../../src/UserSettingsStore';
|
import UserSettingsStore from '../../../../src/UserSettingsStore';
|
||||||
const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput');
|
const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput');
|
||||||
import MatrixClientPeg from '../../../../src/MatrixClientPeg';
|
import MatrixClientPeg from '../../../../src/MatrixClientPeg';
|
||||||
|
import RoomMember from 'matrix-js-sdk';
|
||||||
|
|
||||||
function addTextToDraft(text) {
|
function addTextToDraft(text) {
|
||||||
const components = document.getElementsByClassName('public-DraftEditor-content');
|
const components = document.getElementsByClassName('public-DraftEditor-content');
|
||||||
|
@ -31,6 +32,7 @@ describe('MessageComposerInput', () => {
|
||||||
testUtils.beforeEach(this);
|
testUtils.beforeEach(this);
|
||||||
sandbox = testUtils.stubClient(sandbox);
|
sandbox = testUtils.stubClient(sandbox);
|
||||||
client = MatrixClientPeg.get();
|
client = MatrixClientPeg.get();
|
||||||
|
client.credentials = {userId: '@me:domain.com'};
|
||||||
|
|
||||||
parentDiv = document.createElement('div');
|
parentDiv = document.createElement('div');
|
||||||
document.body.appendChild(parentDiv);
|
document.body.appendChild(parentDiv);
|
||||||
|
@ -236,4 +238,68 @@ describe('MessageComposerInput', () => {
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should strip tab-completed mentions so that only the display name is sent in the plain body in Markdown mode', () => {
|
||||||
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
|
const spy = sinon.spy(client, 'sendHtmlMessage');
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
mci.setDisplayedCompletion({
|
||||||
|
completion: 'Some Member',
|
||||||
|
selection: mci.state.editorState.getSelection(),
|
||||||
|
href: `https://matrix.to/#/@some_member:domain.bla`,
|
||||||
|
});
|
||||||
|
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.args[0][1]).toEqual(
|
||||||
|
'Some Member',
|
||||||
|
'the plaintext body should only include the display name',
|
||||||
|
);
|
||||||
|
expect(spy.args[0][2]).toEqual(
|
||||||
|
'<a href="https://matrix.to/#/@some_member:domain.bla">Some Member</a>',
|
||||||
|
'the html body should contain an anchor tag with a matrix.to href and display name text',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should strip tab-completed mentions so that only the display name is sent in the plain body in RTE mode', () => {
|
||||||
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
|
const spy = sinon.spy(client, 'sendHtmlMessage');
|
||||||
|
mci.enableRichtext(true);
|
||||||
|
mci.setDisplayedCompletion({
|
||||||
|
completion: 'Some Member',
|
||||||
|
selection: mci.state.editorState.getSelection(),
|
||||||
|
href: `https://matrix.to/#/@some_member:domain.bla`,
|
||||||
|
});
|
||||||
|
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.args[0][1]).toEqual('Some Member');
|
||||||
|
expect(spy.args[0][2]).toEqual('<a href="https://matrix.to/#/@some_member:domain.bla">Some Member</a>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not strip non-tab-completed mentions when manually typing MD', () => {
|
||||||
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
|
const spy = sinon.spy(client, 'sendHtmlMessage');
|
||||||
|
// Markdown mode enabled
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
||||||
|
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.args[0][1]).toEqual('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
||||||
|
expect(spy.args[0][2]).toEqual('<a href="https://matrix.to/#/@some_member:domain.bla">My Not-Tab-Completed Mention</a>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not strip arbitrary typed (i.e. not tab-completed) MD links', () => {
|
||||||
|
// Sending a HTML message because we have entities in the composer (because of completions)
|
||||||
|
const spy = sinon.spy(client, 'sendHtmlMessage');
|
||||||
|
// Markdown mode enabled
|
||||||
|
mci.enableRichtext(false);
|
||||||
|
addTextToDraft('[Click here](https://some.lovely.url)');
|
||||||
|
|
||||||
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
|
expect(spy.args[0][1]).toEqual('[Click here](https://some.lovely.url)');
|
||||||
|
expect(spy.args[0][2]).toEqual('<a href="https://some.lovely.url">Click here</a>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -238,7 +238,12 @@ export function mkStubRoom(roomId = null) {
|
||||||
return {
|
return {
|
||||||
roomId,
|
roomId,
|
||||||
getReceiptsForEvent: sinon.stub().returns([]),
|
getReceiptsForEvent: sinon.stub().returns([]),
|
||||||
getMember: sinon.stub().returns({}),
|
getMember: sinon.stub().returns({
|
||||||
|
userId: '@member:domain.bla',
|
||||||
|
name: 'Member',
|
||||||
|
roomId: roomId,
|
||||||
|
getAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
|
}),
|
||||||
getJoinedMembers: sinon.stub().returns([]),
|
getJoinedMembers: sinon.stub().returns([]),
|
||||||
getPendingEvents: () => [],
|
getPendingEvents: () => [],
|
||||||
getLiveTimeline: () => stubTimeline,
|
getLiveTimeline: () => stubTimeline,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue