+
+ {avatarNode}
-
- {nameNode}
-
- {summary.profile.short_description}
+
+
+ {nameNode}
+
+
+ {shortDescNode}
-
-
-
+
+
+ {rightButtons}
{roomBody}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index a051af5d08..093fae5d7b 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -307,6 +307,7 @@ export default React.createClass({
page_element =
;
+ //right_panel =
;
break;
}
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 2eabe373f8..9573b9fd9f 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -50,6 +50,7 @@ export default React.createClass({
loading: false,
widgetUrl: this.props.url,
error: null,
+ deleting: false,
};
},
@@ -101,6 +102,7 @@ export default React.createClass({
_onDeleteClick: function() {
console.log("Delete widget %s", this.props.id);
+ this.setState({deleting: true});
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId,
'im.vector.modular.widgets',
@@ -110,6 +112,7 @@ export default React.createClass({
console.log('Deleted widget');
}, (e) => {
console.error('Failed to delete widget', e);
+ this.setState({deleting: false});
});
},
@@ -124,6 +127,12 @@ export default React.createClass({
render: function() {
let appTileBody;
+
+ // Don't render widget if it is in the process of being deleted
+ if (this.state.deleting) {
+ return
;
+ }
+
if (this.state.loading) {
appTileBody = (
Loading...
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 85cc85be49..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);
@@ -297,10 +262,9 @@ module.exports = React.createClass({
onEmoteSenderClick: function(event) {
const mxEvent = this.props.mxEvent;
- const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
dis.dispatch({
- action: 'insert_displayname',
- displayname: name.replace(' (IRC)', ''),
+ action: 'insert_mention',
+ user_id: mxEvent.getSender(),
});
},
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 68ef3a1f44..b3831a7d0d 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -357,10 +357,10 @@ module.exports = withMatrixClient(React.createClass({
},
onSenderProfileClick: function(event) {
- var mxEvent = this.props.mxEvent;
+ const mxEvent = this.props.mxEvent;
dis.dispatch({
- action: 'insert_displayname',
- displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
+ action: 'insert_mention',
+ user_id: mxEvent.getSender(),
});
},
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 06c854a69f..e19e5fb1b9 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}
);
},
@@ -285,22 +238,20 @@ export default class MessageComposerInput extends React.Component {
case 'focus_composer':
editor.focus();
break;
-
- // TODO change this so we insert a complete user alias
-
- case 'insert_displayname': {
- contentState = Modifier.replaceText(
- contentState,
- this.state.editorState.getSelection(),
- `${payload.displayname}: `,
- );
- let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
- editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
- this.onEditorContentChanged(editorState);
- editor.focus();
+ case 'insert_mention': {
+ // Pretend that we've autocompleted this user because keeping two code
+ // paths for inserting a user pill is not fun
+ const selection = this.state.editorState.getSelection();
+ const member = this.props.room.getMember(payload.user_id);
+ const completion = member ? member.name.replace(' (IRC)', '') : payload.user_id;
+ this.setDisplayedCompletion({
+ completion,
+ selection,
+ href: `https://matrix.to/#/${payload.user_id}`,
+ suffix: selection.getStartOffset() === 0 ? ': ' : ' ',
+ });
}
break;
-
case 'quote': {
let {body, formatted_body} = payload.event.getContent();
formatted_body = formatted_body || escape(body);
@@ -775,6 +726,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(
@@ -933,21 +913,32 @@ export default class MessageComposerInput extends React.Component {
return false;
}
- const {range = {}, completion = '', href = null, suffix = ''} = displayedCompletion;
+ const {range = null, completion = '', href = null, suffix = ''} = displayedCompletion;
+
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})`;
}
}
+ let selection;
+ if (range) {
+ selection = RichText.textOffsetsToSelectionState(
+ range, activeEditorState.getCurrentContent().getBlocksAsArray(),
+ );
+ } else {
+ selection = activeEditorState.getSelection();
+ }
+
let contentState = Modifier.replaceText(
activeEditorState.getCurrentContent(),
- RichText.textOffsetsToSelectionState(
- range, activeEditorState.getCurrentContent().getBlocksAsArray(),
- ),
+ selection,
mdCompletion || completion,
null,
entityKey,
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 4841f2f0de..c6c563ac5b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -956,5 +956,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,