diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index 20b444b8da..87e714083b 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -145,7 +145,7 @@ const sanitizeHtmlParams = {
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
- img: ['src'],
+ img: ['src', 'width', 'height', 'alt', 'title'],
ol: ['start'],
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
},
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 30d67202e7..5f7866773d 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -22,6 +22,8 @@ import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton';
+import Modal from '../../Modal';
+import classnames from 'classnames';
const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired,
@@ -179,10 +181,13 @@ export default React.createClass({
summary: null,
error: null,
editing: false,
+ saving: false,
+ uploadingAvatar: false,
};
},
componentWillMount: function() {
+ this._changeAvatarComponent = null;
this._loadGroupFromServer(this.props.groupId);
},
@@ -211,8 +216,83 @@ export default React.createClass({
});
},
- _onSettingsClick: function() {
- this.setState({editing: true});
+ _onEditClick: function() {
+ 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() {
@@ -296,60 +376,129 @@ export default React.createClass({
const Loader = sdk.getComponent("elements.Spinner");
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 ;
- } else if (this.state.editing) {
- return
;
} else if (this.state.summary) {
const summary = this.state.summary;
- let description = null;
- if (summary.profile && summary.profile.long_description) {
- description = sanitizedHtmlNode(summary.profile.long_description);
- }
-
- const roomBody =
-
{description}
- {this._getFeaturedRoomsNode()}
- {this._getFeaturedUsersNode()}
-
;
+ let avatarNode;
let nameNode;
- if (summary.profile && summary.profile.name) {
- nameNode =
-
{summary.profile.name}
-
- ({this.props.groupId})
-
+ let shortDescNode;
+ let rightButtons;
+ let roomBody;
+ const headerClasses = {
+ mx_GroupView_header: true,
+ };
+ if (this.state.editing) {
+ let avatarImage;
+ if (this.state.uploadingAvatar) {
+ avatarImage =
;
+ } else {
+ const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
+ avatarImage =
;
+ }
+
+ avatarNode = (
+
+
+
+
+
+
+
+ );
+ nameNode =
;
+ shortDescNode =
;
+ rightButtons =
+
+ {_t('Save')}
+
+
+
+
+ ;
+ roomBody =
+
;
} else {
- nameNode =
-
{this.props.groupId}
+ const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
+ avatarNode =
;
+ if (summary.profile && summary.profile.name) {
+ nameNode =
+ {summary.profile.name}
+
+ ({this.props.groupId})
+
+
;
+ } else {
+ nameNode =
{this.props.groupId};
+ }
+ shortDescNode =
{summary.profile.short_description};
+
+ let description = null;
+ if (summary.profile && summary.profile.long_description) {
+ description = sanitizedHtmlNode(summary.profile.long_description);
+ }
+ roomBody =
+
{description}
+ {this._getFeaturedRoomsNode()}
+ {this._getFeaturedUsersNode()}
;
+ // disabled until editing works
+ rightButtons =
+
+ ;
+
+ 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 (
-
-
-
-
+
+
+
+ {avatarNode}
-
- {nameNode}
-
- {summary.profile.short_description}
+
+
+ {nameNode}
+
+
+ {shortDescNode}
-
-
-
+
+
+ {rightButtons}
{roomBody}
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
new file mode 100644
index 0000000000..fdd657470e
--- /dev/null
+++ b/src/components/views/elements/Pill.js
@@ -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 =
;
+ pillClass = 'mx_UserPill';
+ }
+ }
+ break;
+ case Pill.TYPE_ROOM_MENTION: {
+ const room = this.state.room;
+ if (room) {
+ linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
+ avatar = ;
+ pillClass = 'mx_RoomPill';
+ }
+ }
+ break;
+ }
+
+ const classes = classNames(pillClass, {
+ "mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
+ });
+
+ if (this.state.pillType) {
+ return this.props.inMessage ?
+
+ {avatar}
+ {linkText}
+ :
+
+ {avatar}
+ {linkText}
+ ;
+ } else {
+ // Deliberately render nothing if the URL isn't recognised
+ return null;
+ }
+ },
+});
+
+export default Pill;
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index b901f98f19..6d4d01a196 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -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 = ;
- break;
- case "room":
- room = resourceId[0] === '#' ?
- MatrixClientPeg.get().getRooms().find((r) => {
- return r.getCanonicalAlias() === resourceId;
- }) : MatrixClientPeg.get().getRoom(resourceId);
- if (room) {
- avatar = ;
- }
- 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 = ;
+
+ ReactDOM.render(pill, pillContainer);
+ node.parentNode.replaceChild(pillContainer, node);
}
} else if (node.children && node.children.length) {
this.pillifyLinks(node.children);
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 2c011f3770..5e451e07ac 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -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,10 @@ 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';
+import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
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';
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
@@ -187,62 +186,16 @@ export default class MessageComposerInput extends React.Component {
RichText.getScopedMDDecorators(this.props);
decorators.push({
strategy: this.findLinkEntities.bind(this),
- component: (props) => {
- const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
- const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
- 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 ? : 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 ? : null;
- }
-
- if (isUserPill || isRoomPill) {
- return (
-
- {avatar}
- {linkText}
-
- );
+ component: (entityProps) => {
+ const Pill = sdk.getComponent('elements.Pill');
+ const {url} = Entity.get(entityProps.entityKey).getData();
+ if (Pill.isPillUrl(url)) {
+ return ;
}
return (
-
- {props.children}
+
+ {entityProps.children}
);
},
@@ -778,6 +731,35 @@ export default class MessageComposerInput extends React.Component {
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;
if (contentHTML) {
sendMessagePromise = sendHtmlFn.call(
@@ -941,7 +923,10 @@ export default class MessageComposerInput extends React.Component {
let entityKey;
let mdCompletion;
if (href) {
- entityKey = Entity.create('LINK', 'IMMUTABLE', {url: href});
+ entityKey = Entity.create('LINK', 'IMMUTABLE', {
+ url: href,
+ isCompletion: true,
+ });
if (!this.state.isRichtextEnabled) {
mdCompletion = `[${completion}](${href})`;
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index da3d1545ae..86cf8727bb 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -958,5 +958,8 @@
"Featured Rooms:": "Featured Rooms:",
"Error whilst fetching joined groups": "Error whilst fetching joined groups",
"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"
}
diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js
index 01512a771a..e395b7986e 100644
--- a/src/linkify-matrix.js
+++ b/src/linkify-matrix.js
@@ -168,6 +168,8 @@ matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
+ ")(#.*)";
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.options = {
diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js
index fe379afcff..e5d98e26c6 100644
--- a/test/components/views/rooms/MessageComposerInput-test.js
+++ b/test/components/views/rooms/MessageComposerInput-test.js
@@ -9,6 +9,7 @@ import sdk from 'matrix-react-sdk';
import UserSettingsStore from '../../../../src/UserSettingsStore';
const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput');
import MatrixClientPeg from '../../../../src/MatrixClientPeg';
+import RoomMember from 'matrix-js-sdk';
function addTextToDraft(text) {
const components = document.getElementsByClassName('public-DraftEditor-content');
@@ -31,6 +32,7 @@ describe('MessageComposerInput', () => {
testUtils.beforeEach(this);
sandbox = testUtils.stubClient(sandbox);
client = MatrixClientPeg.get();
+ client.credentials = {userId: '@me:domain.com'};
parentDiv = document.createElement('div');
document.body.appendChild(parentDiv);
@@ -236,4 +238,68 @@ describe('MessageComposerInput', () => {
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.');
});
+
+ 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(
+ 'Some Member',
+ '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('Some Member');
+ });
+
+ 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('My Not-Tab-Completed Mention');
+ });
+
+ 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('Click here');
+ });
});
diff --git a/test/test-utils.js b/test/test-utils.js
index 23f16a2e4c..06d3c03c49 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -238,7 +238,12 @@ export function mkStubRoom(roomId = null) {
return {
roomId,
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([]),
getPendingEvents: () => [],
getLiveTimeline: () => stubTimeline,