diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index b7d775fff7..783a9ec3eb 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -170,11 +170,11 @@ const Pill = React.createClass({
if (this.state.pillType) {
return this.props.inMessage ?
-
+
{avatar}
{linkText}
:
-
+
{avatar}
{linkText}
;
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 006aebff60..2863b13243 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -43,6 +43,10 @@ import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore';
+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);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
@@ -727,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(
@@ -890,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/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,