Merge remote-tracking branch 'origin/develop' into develop
						commit
						6504201190
					
				| 
						 | 
				
			
			@ -53,7 +53,7 @@
 | 
			
		|||
@import "./views/elements/_InlineSpinner.scss";
 | 
			
		||||
@import "./views/elements/_MemberEventListSummary.scss";
 | 
			
		||||
@import "./views/elements/_ProgressBar.scss";
 | 
			
		||||
@import "./views/elements/_Quote.scss";
 | 
			
		||||
@import "./views/elements/_ReplyThread.scss";
 | 
			
		||||
@import "./views/elements/_RichText.scss";
 | 
			
		||||
@import "./views/elements/_RoleButton.scss";
 | 
			
		||||
@import "./views/elements/_Spinner.scss";
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +89,7 @@
 | 
			
		|||
@import "./views/rooms/_PinnedEventTile.scss";
 | 
			
		||||
@import "./views/rooms/_PinnedEventsPanel.scss";
 | 
			
		||||
@import "./views/rooms/_PresenceLabel.scss";
 | 
			
		||||
@import "./views/rooms/_QuotePreview.scss";
 | 
			
		||||
@import "./views/rooms/_ReplyPreview.scss";
 | 
			
		||||
@import "./views/rooms/_RoomDropTarget.scss";
 | 
			
		||||
@import "./views/rooms/_RoomHeader.scss";
 | 
			
		||||
@import "./views/rooms/_RoomList.scss";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2017 Vector Creations Ltd
 | 
			
		||||
Copyright 2018 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -14,13 +14,19 @@ See the License for the specific language governing permissions and
 | 
			
		|||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.mx_Quote .mx_DateSeparator {
 | 
			
		||||
.mx_ReplyThread .mx_DateSeparator {
 | 
			
		||||
    font-size: 1em !important;
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
    padding-bottom: 1px;
 | 
			
		||||
    bottom: -5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_Quote_show {
 | 
			
		||||
.mx_ReplyThread_show {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
blockquote.mx_ReplyThread {
 | 
			
		||||
    margin-left: 0;
 | 
			
		||||
    padding-left: 10px;
 | 
			
		||||
    border-left: 4px solid $blockquote-bar-color;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ limitations under the License.
 | 
			
		|||
    position: absolute;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile_line {
 | 
			
		||||
.mx_EventTile_line, .mx_EventTile_reply {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
 | 
			
		||||
    margin-right: 110px;
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +96,7 @@ limitations under the License.
 | 
			
		|||
    line-height: 22px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile_quote {
 | 
			
		||||
.mx_EventTile_reply {
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +119,7 @@ limitations under the License.
 | 
			
		|||
    background-color: $event-selected-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile:hover .mx_EventTile_line:not(.mx_EventTile_quote),
 | 
			
		||||
.mx_EventTile:hover .mx_EventTile_line,
 | 
			
		||||
.mx_EventTile.menu .mx_EventTile_line
 | 
			
		||||
{
 | 
			
		||||
    background-color: $event-selected-color;
 | 
			
		||||
| 
						 | 
				
			
			@ -157,7 +157,8 @@ limitations under the License.
 | 
			
		|||
    color: $event-notsent-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile_redacted .mx_EventTile_line .mx_UnknownBody {
 | 
			
		||||
.mx_EventTile_redacted .mx_EventTile_line .mx_UnknownBody,
 | 
			
		||||
.mx_EventTile_redacted .mx_EventTile_reply .mx_UnknownBody {
 | 
			
		||||
    display: block;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 22px;
 | 
			
		||||
| 
						 | 
				
			
			@ -202,10 +203,10 @@ limitations under the License.
 | 
			
		|||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile_last .mx_MessageTimestamp,
 | 
			
		||||
.mx_EventTile:hover .mx_MessageTimestamp,
 | 
			
		||||
.mx_EventTile.menu .mx_MessageTimestamp
 | 
			
		||||
{
 | 
			
		||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
 | 
			
		||||
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
 | 
			
		||||
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
 | 
			
		||||
.mx_EventTile.menu > div > a > .mx_MessageTimestamp {
 | 
			
		||||
    visibility: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -235,12 +236,7 @@ limitations under the License.
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile:hover .mx_EventTile_editButton,
 | 
			
		||||
.mx_EventTile.menu .mx_EventTile_editButton
 | 
			
		||||
{
 | 
			
		||||
    visibility: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile.menu .mx_MessageTimestamp {
 | 
			
		||||
.mx_EventTile.menu .mx_EventTile_editButton {
 | 
			
		||||
    visibility: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -358,8 +354,9 @@ limitations under the License.
 | 
			
		|||
    border-left: $e2e-unverified-color 5px solid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_verified .mx_MessageTimestamp,
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_MessageTimestamp {
 | 
			
		||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp {
 | 
			
		||||
    left: 3px;
 | 
			
		||||
    width: auto;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -370,8 +367,9 @@ limitations under the License.
 | 
			
		|||
}
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_e2eIcon,
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_e2eIcon {
 | 
			
		||||
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
 | 
			
		||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon {
 | 
			
		||||
    display: block;
 | 
			
		||||
    left: 41px;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -466,7 +464,7 @@ limitations under the License.
 | 
			
		|||
        // same as the padding for non-compact .mx_EventTile.mx_EventTile_info
 | 
			
		||||
        padding-top: 0px;
 | 
			
		||||
        font-size: 13px;
 | 
			
		||||
        .mx_EventTile_line {
 | 
			
		||||
        .mx_EventTile_line, .mx_EventTile_reply {
 | 
			
		||||
            line-height: 20px;
 | 
			
		||||
        }
 | 
			
		||||
        .mx_EventTile_avatar {
 | 
			
		||||
| 
						 | 
				
			
			@ -484,7 +482,7 @@ limitations under the License.
 | 
			
		|||
        .mx_EventTile_avatar {
 | 
			
		||||
            top: 2px;
 | 
			
		||||
        }
 | 
			
		||||
        .mx_EventTile_line {
 | 
			
		||||
        .mx_EventTile_line, .mx_EventTile_reply {
 | 
			
		||||
            padding-top: 0px;
 | 
			
		||||
            padding-bottom: 1px;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -492,13 +490,13 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
    .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
        .mx_EventTile_line {
 | 
			
		||||
        .mx_EventTile_line, .mx_EventTile_reply {
 | 
			
		||||
            padding-top: 0px;
 | 
			
		||||
            padding-bottom: 0px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .mx_EventTile_line {
 | 
			
		||||
    .mx_EventTile_line, .mx_EventTile_reply {
 | 
			
		||||
        padding-top: 0px;
 | 
			
		||||
        padding-bottom: 0px;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
.mx_QuotePreview {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    z-index: 1000;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    border: 1px solid $primary-hairline-color;
 | 
			
		||||
    background: $primary-bg-color;
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
    border-radius: 4px 4px 0 0;
 | 
			
		||||
    max-height: 50vh;
 | 
			
		||||
    overflow: auto
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_QuotePreview_section {
 | 
			
		||||
    border-bottom: 1px solid $primary-hairline-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_QuotePreview_header {
 | 
			
		||||
    margin: 12px;
 | 
			
		||||
    color: $primary-fg-color;
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
    opacity: 0.4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_QuotePreview_title {
 | 
			
		||||
    float: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_QuotePreview_cancel {
 | 
			
		||||
    float: right;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_QuotePreview_clear {
 | 
			
		||||
    clear: both;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2018 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.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.mx_ReplyPreview {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    z-index: 1000;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    border: 1px solid $primary-hairline-color;
 | 
			
		||||
    background: $primary-bg-color;
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
    border-radius: 4px 4px 0 0;
 | 
			
		||||
    max-height: 50vh;
 | 
			
		||||
    overflow: auto
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_ReplyPreview_section {
 | 
			
		||||
    border-bottom: 1px solid $primary-hairline-color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_ReplyPreview_header {
 | 
			
		||||
    margin: 12px;
 | 
			
		||||
    color: $primary-fg-color;
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
    opacity: 0.4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_ReplyPreview_title {
 | 
			
		||||
    float: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_ReplyPreview_cancel {
 | 
			
		||||
    float: right;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mx_ReplyPreview_clear {
 | 
			
		||||
    clear: both;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +17,8 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
import ReplyThread from "./components/views/elements/ReplyThread";
 | 
			
		||||
 | 
			
		||||
const React = require('react');
 | 
			
		||||
const sanitizeHtml = require('sanitize-html');
 | 
			
		||||
const highlight = require('highlight.js');
 | 
			
		||||
| 
						 | 
				
			
			@ -184,6 +186,7 @@ const sanitizeHtmlParams = {
 | 
			
		|||
    ],
 | 
			
		||||
    allowedAttributes: {
 | 
			
		||||
        // custom ones first:
 | 
			
		||||
        blockquote: ['data-mx-reply'], // used to allow explicit removal of a reply fallback blockquote, value ignored
 | 
			
		||||
        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
 | 
			
		||||
| 
						 | 
				
			
			@ -408,12 +411,14 @@ class TextHighlighter extends BaseHighlighter {
 | 
			
		|||
     *
 | 
			
		||||
     * opts.highlightLink: optional href to add to highlighted words
 | 
			
		||||
     * opts.disableBigEmoji: optional argument to disable the big emoji class.
 | 
			
		||||
     * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
 | 
			
		||||
     */
 | 
			
		||||
export function bodyToHtml(content, highlights, opts={}) {
 | 
			
		||||
    let isHtml = (content.format === "org.matrix.custom.html");
 | 
			
		||||
    let isHtml = content.format === "org.matrix.custom.html" && content.formatted_body;
 | 
			
		||||
 | 
			
		||||
    let bodyHasEmoji = false;
 | 
			
		||||
 | 
			
		||||
    let strippedBody;
 | 
			
		||||
    let safeBody;
 | 
			
		||||
    // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
 | 
			
		||||
    // to highlight HTML tags themselves.  However, this does mean that we don't highlight textnodes which
 | 
			
		||||
| 
						 | 
				
			
			@ -431,17 +436,22 @@ export function bodyToHtml(content, highlights, opts={}) {
 | 
			
		|||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        bodyHasEmoji = containsEmoji(isHtml ? content.formatted_body : content.body);
 | 
			
		||||
        let formattedBody = content.formatted_body;
 | 
			
		||||
        if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
 | 
			
		||||
        strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
 | 
			
		||||
 | 
			
		||||
        bodyHasEmoji = containsEmoji(isHtml ? formattedBody : content.body);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // Only generate safeBody if the message was sent as org.matrix.custom.html
 | 
			
		||||
        if (isHtml) {
 | 
			
		||||
            safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
 | 
			
		||||
            safeBody = sanitizeHtml(formattedBody, sanitizeHtmlParams);
 | 
			
		||||
        } else {
 | 
			
		||||
            // ... or if there are emoji, which we insert as HTML alongside the
 | 
			
		||||
            // escaped plaintext body.
 | 
			
		||||
            if (bodyHasEmoji) {
 | 
			
		||||
                isHtml = true;
 | 
			
		||||
                safeBody = sanitizeHtml(escape(content.body), sanitizeHtmlParams);
 | 
			
		||||
                safeBody = sanitizeHtml(escape(strippedBody), sanitizeHtmlParams);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -458,7 +468,7 @@ export function bodyToHtml(content, highlights, opts={}) {
 | 
			
		|||
    let emojiBody = false;
 | 
			
		||||
    if (!opts.disableBigEmoji && bodyHasEmoji) {
 | 
			
		||||
        EMOJI_REGEX.lastIndex = 0;
 | 
			
		||||
        const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
 | 
			
		||||
        const contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
 | 
			
		||||
        const match = EMOJI_REGEX.exec(contentBodyTrimmed);
 | 
			
		||||
        emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -471,7 +481,7 @@ export function bodyToHtml(content, highlights, opts={}) {
 | 
			
		|||
 | 
			
		||||
    return isHtml ?
 | 
			
		||||
        <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
 | 
			
		||||
        <span className={className} dir="auto">{ content.body }</span>;
 | 
			
		||||
        <span className={className} dir="auto">{ strippedBody }</span>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function emojifyText(text) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -908,17 +908,17 @@ module.exports = React.createClass({
 | 
			
		|||
        this.setState({ draggingFile: false });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    uploadFile: function(file) {
 | 
			
		||||
    uploadFile: async function(file) {
 | 
			
		||||
        if (MatrixClientPeg.get().isGuest()) {
 | 
			
		||||
            dis.dispatch({action: 'view_set_mxid'});
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ContentMessages.sendContentToRoom(
 | 
			
		||||
            file, this.state.room.roomId, MatrixClientPeg.get(),
 | 
			
		||||
        ).catch((error) => {
 | 
			
		||||
        try {
 | 
			
		||||
            await ContentMessages.sendContentToRoom(file, this.state.room.roomId, MatrixClientPeg.get());
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (error.name === "UnknownDeviceError") {
 | 
			
		||||
                // Let the staus bar handle this
 | 
			
		||||
                // Let the status bar handle this
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
			
		||||
| 
						 | 
				
			
			@ -928,6 +928,14 @@ module.exports = React.createClass({
 | 
			
		|||
                description: ((error && error.message)
 | 
			
		||||
                    ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // bail early to avoid calling the dispatch below
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Send message_sent callback, for things like _checkIfAlone because after all a file is still a message.
 | 
			
		||||
        dis.dispatch({
 | 
			
		||||
            action: 'message_sent',
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ limitations under the License.
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import MatrixClientPeg from '../../../MatrixClientPeg';
 | 
			
		||||
import dis from '../../../dispatcher';
 | 
			
		||||
| 
						 | 
				
			
			@ -34,13 +35,16 @@ module.exports = React.createClass({
 | 
			
		|||
 | 
			
		||||
    propTypes: {
 | 
			
		||||
        /* the MatrixEvent associated with the context menu */
 | 
			
		||||
        mxEvent: React.PropTypes.object.isRequired,
 | 
			
		||||
        mxEvent: PropTypes.object.isRequired,
 | 
			
		||||
 | 
			
		||||
        /* an optional EventTileOps implementation that can be used to unhide preview widgets */
 | 
			
		||||
        eventTileOps: React.PropTypes.object,
 | 
			
		||||
        eventTileOps: PropTypes.object,
 | 
			
		||||
 | 
			
		||||
        /* an optional function to be called when the user clicks collapse thread, if not provided hide button */
 | 
			
		||||
        collapseReplyThread: PropTypes.func,
 | 
			
		||||
 | 
			
		||||
        /* callback called when the menu is dismissed */
 | 
			
		||||
        onFinished: React.PropTypes.func,
 | 
			
		||||
        onFinished: PropTypes.func,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getInitialState: function() {
 | 
			
		||||
| 
						 | 
				
			
			@ -182,12 +186,17 @@ module.exports = React.createClass({
 | 
			
		|||
 | 
			
		||||
    onReplyClick: function() {
 | 
			
		||||
        dis.dispatch({
 | 
			
		||||
            action: 'quote_event',
 | 
			
		||||
            action: 'reply_to_event',
 | 
			
		||||
            event: this.props.mxEvent,
 | 
			
		||||
        });
 | 
			
		||||
        this.closeMenu();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    onCollapseReplyThreadClick: function() {
 | 
			
		||||
        this.props.collapseReplyThread();
 | 
			
		||||
        this.closeMenu();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    render: function() {
 | 
			
		||||
        const eventStatus = this.props.mxEvent.status;
 | 
			
		||||
        let resendButton;
 | 
			
		||||
| 
						 | 
				
			
			@ -200,6 +209,7 @@ module.exports = React.createClass({
 | 
			
		|||
        let externalURLButton;
 | 
			
		||||
        let quoteButton;
 | 
			
		||||
        let replyButton;
 | 
			
		||||
        let collapseReplyThread;
 | 
			
		||||
 | 
			
		||||
        if (eventStatus === 'not_sent') {
 | 
			
		||||
            resendButton = (
 | 
			
		||||
| 
						 | 
				
			
			@ -305,6 +315,13 @@ module.exports = React.createClass({
 | 
			
		|||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.props.collapseReplyThread) {
 | 
			
		||||
            collapseReplyThread = (
 | 
			
		||||
                <div className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
 | 
			
		||||
                    { _t('Collapse Reply Thread') }
 | 
			
		||||
                </div>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div>
 | 
			
		||||
| 
						 | 
				
			
			@ -320,6 +337,7 @@ module.exports = React.createClass({
 | 
			
		|||
                { quoteButton }
 | 
			
		||||
                { replyButton }
 | 
			
		||||
                { externalURLButton }
 | 
			
		||||
                { collapseReplyThread }
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,188 +0,0 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2017 New Vector 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 {_t} from '../../../languageHandler';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import MatrixClientPeg from '../../../MatrixClientPeg';
 | 
			
		||||
import {wantsDateSeparator} from '../../../DateUtils';
 | 
			
		||||
import {MatrixEvent} from 'matrix-js-sdk';
 | 
			
		||||
import {makeUserPermalink} from "../../../matrix-to";
 | 
			
		||||
 | 
			
		||||
// For URLs of matrix.to links in the timeline which have been reformatted by
 | 
			
		||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
 | 
			
		||||
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
 | 
			
		||||
 | 
			
		||||
export default class Quote extends React.Component {
 | 
			
		||||
    static isMessageUrl(url) {
 | 
			
		||||
        return !!REGEX_LOCAL_MATRIXTO.exec(url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static childContextTypes = {
 | 
			
		||||
        matrixClient: PropTypes.object,
 | 
			
		||||
        addRichQuote: PropTypes.func,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    static propTypes = {
 | 
			
		||||
        // The matrix.to url of the event
 | 
			
		||||
        url: PropTypes.string,
 | 
			
		||||
        // The original node that was rendered
 | 
			
		||||
        node: PropTypes.instanceOf(Element),
 | 
			
		||||
        // The parent event
 | 
			
		||||
        parentEv: PropTypes.instanceOf(MatrixEvent),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            // The event related to this quote and their nested rich quotes
 | 
			
		||||
            events: [],
 | 
			
		||||
            // Whether the top (oldest) event should be shown or spoilered
 | 
			
		||||
            show: true,
 | 
			
		||||
            // Whether an error was encountered fetching nested older event, show node if it does
 | 
			
		||||
            err: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.onQuoteClick = this.onQuoteClick.bind(this);
 | 
			
		||||
        this.addRichQuote = this.addRichQuote.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChildContext() {
 | 
			
		||||
        return {
 | 
			
		||||
            matrixClient: MatrixClientPeg.get(),
 | 
			
		||||
            addRichQuote: this.addRichQuote,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    parseUrl(url) {
 | 
			
		||||
        if (!url) return;
 | 
			
		||||
 | 
			
		||||
        // Default to the empty array if no match for simplicity
 | 
			
		||||
        // resource and prefix will be undefined instead of throwing
 | 
			
		||||
        const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
 | 
			
		||||
 | 
			
		||||
        const [, roomIdentifier, eventId] = matrixToMatch;
 | 
			
		||||
        return {roomIdentifier, eventId};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillReceiveProps(nextProps) {
 | 
			
		||||
        const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
 | 
			
		||||
        if (!roomIdentifier || !eventId) return;
 | 
			
		||||
 | 
			
		||||
        const room = this.getRoom(roomIdentifier);
 | 
			
		||||
        if (!room) return;
 | 
			
		||||
 | 
			
		||||
        // Only try and load the event if we know about the room
 | 
			
		||||
        // otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
 | 
			
		||||
        this.setState({ events: [] });
 | 
			
		||||
        if (room) this.getEvent(room, eventId, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillMount() {
 | 
			
		||||
        this.componentWillReceiveProps(this.props);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getRoom(id) {
 | 
			
		||||
        const cli = MatrixClientPeg.get();
 | 
			
		||||
        if (id[0] === '!') return cli.getRoom(id);
 | 
			
		||||
 | 
			
		||||
        return cli.getRooms().find((r) => {
 | 
			
		||||
            return r.getAliases().includes(id);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getEvent(room, eventId, show) {
 | 
			
		||||
        const event = room.findEventById(eventId);
 | 
			
		||||
        if (event) {
 | 
			
		||||
            this.addEvent(event, show);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
 | 
			
		||||
        this.addEvent(room.findEventById(eventId), show);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addEvent(event, show) {
 | 
			
		||||
        const events = [event].concat(this.state.events);
 | 
			
		||||
        this.setState({events, show});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // addRichQuote(roomId, eventId) {
 | 
			
		||||
    addRichQuote(href) {
 | 
			
		||||
        const {roomIdentifier, eventId} = this.parseUrl(href);
 | 
			
		||||
        if (!roomIdentifier || !eventId) {
 | 
			
		||||
            this.setState({ err: true });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const room = this.getRoom(roomIdentifier);
 | 
			
		||||
        if (!room) {
 | 
			
		||||
            this.setState({ err: true });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.getEvent(room, eventId, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onQuoteClick() {
 | 
			
		||||
        this.setState({ show: true });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const events = this.state.events.slice();
 | 
			
		||||
        if (events.length) {
 | 
			
		||||
            const evTiles = [];
 | 
			
		||||
 | 
			
		||||
            if (!this.state.show) {
 | 
			
		||||
                const oldestEv = events.shift();
 | 
			
		||||
                const Pill = sdk.getComponent('elements.Pill');
 | 
			
		||||
                const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
 | 
			
		||||
 | 
			
		||||
                evTiles.push(<blockquote className="mx_Quote" key="load">
 | 
			
		||||
                    {
 | 
			
		||||
                        _t('<a>In reply to</a> <pill>', {}, {
 | 
			
		||||
                            'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
 | 
			
		||||
                            'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
 | 
			
		||||
                                          url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
 | 
			
		||||
                        })
 | 
			
		||||
                    }
 | 
			
		||||
                </blockquote>);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const EventTile = sdk.getComponent('views.rooms.EventTile');
 | 
			
		||||
            const DateSeparator = sdk.getComponent('messages.DateSeparator');
 | 
			
		||||
            events.forEach((ev) => {
 | 
			
		||||
                let dateSep = null;
 | 
			
		||||
 | 
			
		||||
                if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
 | 
			
		||||
                    dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
 | 
			
		||||
                    { dateSep }
 | 
			
		||||
                    <EventTile mxEvent={ev} tileShape="quote" />
 | 
			
		||||
                </blockquote>);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return <div>{ evTiles }</div>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Deliberately render nothing if the URL isn't recognised
 | 
			
		||||
        // in case we get an undefined/falsey node, replace it with null to make React happy
 | 
			
		||||
        return this.props.node || null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,306 @@
 | 
			
		|||
/*
 | 
			
		||||
Copyright 2017 New Vector 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 {_t} from '../../../languageHandler';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import dis from '../../../dispatcher';
 | 
			
		||||
import {wantsDateSeparator} from '../../../DateUtils';
 | 
			
		||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
 | 
			
		||||
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
 | 
			
		||||
import SettingsStore from "../../../settings/SettingsStore";
 | 
			
		||||
 | 
			
		||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
 | 
			
		||||
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
 | 
			
		||||
// be low as each event being loaded (after the first) is triggered by an explicit user action.
 | 
			
		||||
export default class ReplyThread extends React.Component {
 | 
			
		||||
    static propTypes = {
 | 
			
		||||
        // the latest event in this chain of replies
 | 
			
		||||
        parentEv: PropTypes.instanceOf(MatrixEvent),
 | 
			
		||||
        // called when the ReplyThread contents has changed, including EventTiles thereof
 | 
			
		||||
        onWidgetLoad: PropTypes.func.isRequired,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    static contextTypes = {
 | 
			
		||||
        matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            // The loaded events to be rendered as linear-replies
 | 
			
		||||
            events: [],
 | 
			
		||||
 | 
			
		||||
            // The latest loaded event which has not yet been shown
 | 
			
		||||
            loadedEv: null,
 | 
			
		||||
            // Whether the component is still loading more events
 | 
			
		||||
            loading: true,
 | 
			
		||||
 | 
			
		||||
            // Whether as error was encountered fetching a replied to event.
 | 
			
		||||
            err: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.onQuoteClick = this.onQuoteClick.bind(this);
 | 
			
		||||
        this.canCollapse = this.canCollapse.bind(this);
 | 
			
		||||
        this.collapse = this.collapse.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static async getEvent(room, eventId) {
 | 
			
		||||
        const event = room.findEventById(eventId);
 | 
			
		||||
        if (event) return event;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // ask the client to fetch the event we want using the context API, only interface to do so is to ask
 | 
			
		||||
            // for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
 | 
			
		||||
            await this.context.matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            // if it fails catch the error and return early, there's no point trying to find the event in this case.
 | 
			
		||||
            // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return room.findEventById(eventId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static getParentEventId(ev) {
 | 
			
		||||
        if (!ev || ev.isRedacted()) return;
 | 
			
		||||
 | 
			
		||||
        const mRelatesTo = ev.getWireContent()['m.relates_to'];
 | 
			
		||||
        if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
 | 
			
		||||
            const mInReplyTo = mRelatesTo['m.in_reply_to'];
 | 
			
		||||
            if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Part of Replies fallback support
 | 
			
		||||
    static stripPlainReply(body) {
 | 
			
		||||
        // Removes lines beginning with `> ` until you reach one that doesn't.
 | 
			
		||||
        const lines = body.split('\n');
 | 
			
		||||
        while (lines.length && lines[0].startsWith('> ')) lines.shift();
 | 
			
		||||
        // Reply fallback has a blank line after it, so remove it to prevent leading newline
 | 
			
		||||
        if (lines[0] === '') lines.shift();
 | 
			
		||||
        return lines.join('\n');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Part of Replies fallback support
 | 
			
		||||
    static stripHTMLReply(html) {
 | 
			
		||||
        return html.replace(/^<blockquote data-mx-reply>[\s\S]+?<\/blockquote>/, '');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Part of Replies fallback support
 | 
			
		||||
    static getNestedReplyText(ev) {
 | 
			
		||||
        if (!ev) return null;
 | 
			
		||||
 | 
			
		||||
        let {body, formatted_body: html} = ev.getContent();
 | 
			
		||||
        if (this.getParentEventId(ev)) {
 | 
			
		||||
            if (body) body = this.stripPlainReply(body);
 | 
			
		||||
            if (html) html = this.stripHTMLReply(html);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const evLink = makeEventPermalink(ev.getRoomId(), ev.getId());
 | 
			
		||||
        const userLink = makeUserPermalink(ev.getSender());
 | 
			
		||||
        const mxid = ev.getSender();
 | 
			
		||||
 | 
			
		||||
        // This fallback contains text that is explicitly EN.
 | 
			
		||||
        switch (ev.getContent().msgtype) {
 | 
			
		||||
            case 'm.text':
 | 
			
		||||
            case 'm.notice': {
 | 
			
		||||
                html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
 | 
			
		||||
                    + `<br>${html || body}</blockquote>`;
 | 
			
		||||
                const lines = body.trim().split('\n');
 | 
			
		||||
                if (lines.length > 0) {
 | 
			
		||||
                    lines[0] = `<${mxid}> ${lines[0]}`;
 | 
			
		||||
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case 'm.image':
 | 
			
		||||
                html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
 | 
			
		||||
                    + `<br>sent an image.</blockquote>`;
 | 
			
		||||
                body = `> <${mxid}> sent an image.\n\n`;
 | 
			
		||||
                break;
 | 
			
		||||
            case 'm.video':
 | 
			
		||||
                html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
 | 
			
		||||
                    + `<br>sent a video.</blockquote>`;
 | 
			
		||||
                body = `> <${mxid}> sent a video.\n\n`;
 | 
			
		||||
                break;
 | 
			
		||||
            case 'm.audio':
 | 
			
		||||
                html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
 | 
			
		||||
                    + `<br>sent an audio file.</blockquote>`;
 | 
			
		||||
                body = `> <${mxid}> sent an audio file.\n\n`;
 | 
			
		||||
                break;
 | 
			
		||||
            case 'm.file':
 | 
			
		||||
                html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
 | 
			
		||||
                    + `<br>sent a file.</blockquote>`;
 | 
			
		||||
                body = `> <${mxid}> sent a file.\n\n`;
 | 
			
		||||
                break;
 | 
			
		||||
            case 'm.emote': {
 | 
			
		||||
                html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> * `
 | 
			
		||||
                    + `<a href="${userLink}">${mxid}</a><br>${html || body}</blockquote>`;
 | 
			
		||||
                const lines = body.trim().split('\n');
 | 
			
		||||
                if (lines.length > 0) {
 | 
			
		||||
                    lines[0] = `* <${mxid}> ${lines[0]}`;
 | 
			
		||||
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            default:
 | 
			
		||||
                return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {body, html};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static makeReplyMixIn(ev) {
 | 
			
		||||
        if (!ev) return {};
 | 
			
		||||
        return {
 | 
			
		||||
            'm.relates_to': {
 | 
			
		||||
                'm.in_reply_to': {
 | 
			
		||||
                    'event_id': ev.getId(),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static makeThread(parentEv, onWidgetLoad, ref) {
 | 
			
		||||
        if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !ReplyThread.getParentEventId(parentEv)) {
 | 
			
		||||
            return <div />;
 | 
			
		||||
        }
 | 
			
		||||
        return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillMount() {
 | 
			
		||||
        this.unmounted = false;
 | 
			
		||||
        this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId());
 | 
			
		||||
        this.initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentDidUpdate() {
 | 
			
		||||
        this.props.onWidgetLoad();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        this.unmounted = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async initialize() {
 | 
			
		||||
        const {parentEv} = this.props;
 | 
			
		||||
        // at time of making this component we checked that props.parentEv has a parentEventId
 | 
			
		||||
        const ev = await ReplyThread.getEvent(this.room, ReplyThread.getParentEventId(parentEv));
 | 
			
		||||
        if (this.unmounted) return;
 | 
			
		||||
 | 
			
		||||
        if (ev) {
 | 
			
		||||
            this.setState({
 | 
			
		||||
                events: [ev],
 | 
			
		||||
            }, this.loadNextEvent);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.setState({err: true});
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async loadNextEvent() {
 | 
			
		||||
        if (this.unmounted) return;
 | 
			
		||||
        const ev = this.state.events[0];
 | 
			
		||||
        const inReplyToEventId = ReplyThread.getParentEventId(ev);
 | 
			
		||||
 | 
			
		||||
        if (!inReplyToEventId) {
 | 
			
		||||
            this.setState({
 | 
			
		||||
                loading: false,
 | 
			
		||||
            });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const loadedEv = await ReplyThread.getEvent(this.room, inReplyToEventId);
 | 
			
		||||
        if (this.unmounted) return;
 | 
			
		||||
 | 
			
		||||
        if (loadedEv) {
 | 
			
		||||
            this.setState({loadedEv});
 | 
			
		||||
        } else {
 | 
			
		||||
            this.setState({err: true});
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    canCollapse() {
 | 
			
		||||
        return this.state.events.length > 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    collapse() {
 | 
			
		||||
        this.initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onQuoteClick() {
 | 
			
		||||
        const events = [this.state.loadedEv, ...this.state.events];
 | 
			
		||||
 | 
			
		||||
        this.setState({
 | 
			
		||||
            loadedEv: null,
 | 
			
		||||
            events,
 | 
			
		||||
        }, this.loadNextEvent);
 | 
			
		||||
 | 
			
		||||
        dis.dispatch({action: 'focus_composer'});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        let header = null;
 | 
			
		||||
 | 
			
		||||
        if (this.state.err) {
 | 
			
		||||
            header = <blockquote className="mx_ReplyThread mx_ReplyThread_error">
 | 
			
		||||
                {
 | 
			
		||||
                    _t('Unable to load event that was replied to, ' +
 | 
			
		||||
                        'it either does not exist or you do not have permission to view it.')
 | 
			
		||||
                }
 | 
			
		||||
            </blockquote>;
 | 
			
		||||
        } else if (this.state.loadedEv) {
 | 
			
		||||
            const ev = this.state.loadedEv;
 | 
			
		||||
            const Pill = sdk.getComponent('elements.Pill');
 | 
			
		||||
            const room = this.context.matrixClient.getRoom(ev.getRoomId());
 | 
			
		||||
            header = <blockquote className="mx_ReplyThread">
 | 
			
		||||
                {
 | 
			
		||||
                    _t('<a>In reply to</a> <pill>', {}, {
 | 
			
		||||
                        'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
 | 
			
		||||
                        'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
 | 
			
		||||
                                      url={makeUserPermalink(ev.getSender())} shouldShowPillAvatar={true} />,
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            </blockquote>;
 | 
			
		||||
        } else if (this.state.loading) {
 | 
			
		||||
            const Spinner = sdk.getComponent("elements.Spinner");
 | 
			
		||||
            header = <Spinner w={16} h={16} />;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const EventTile = sdk.getComponent('views.rooms.EventTile');
 | 
			
		||||
        const DateSeparator = sdk.getComponent('messages.DateSeparator');
 | 
			
		||||
        const evTiles = this.state.events.map((ev) => {
 | 
			
		||||
            let dateSep = null;
 | 
			
		||||
 | 
			
		||||
            if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
 | 
			
		||||
                dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return <blockquote className="mx_ReplyThread" key={ev.getId()}>
 | 
			
		||||
                { dateSep }
 | 
			
		||||
                <EventTile mxEvent={ev}
 | 
			
		||||
                           tileShape="reply"
 | 
			
		||||
                           onWidgetLoad={this.props.onWidgetLoad}
 | 
			
		||||
                           isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
 | 
			
		||||
            </blockquote>;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return <div>
 | 
			
		||||
            <div>{ header }</div>
 | 
			
		||||
            <div>{ evTiles }</div>
 | 
			
		||||
        </div>;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +35,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
 | 
			
		|||
import ContextualMenu from '../../structures/ContextualMenu';
 | 
			
		||||
import SettingsStore from "../../../settings/SettingsStore";
 | 
			
		||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
 | 
			
		||||
import ReplyThread from "../elements/ReplyThread";
 | 
			
		||||
 | 
			
		||||
linkifyMatrix(linkify);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,10 +62,6 @@ module.exports = React.createClass({
 | 
			
		|||
        tileShape: PropTypes.string,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    contextTypes: {
 | 
			
		||||
        addRichQuote: PropTypes.func,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getInitialState: function() {
 | 
			
		||||
        return {
 | 
			
		||||
            // the URLs (if any) to be previewed with a LinkPreviewWidget
 | 
			
		||||
| 
						 | 
				
			
			@ -186,7 +183,6 @@ module.exports = React.createClass({
 | 
			
		|||
 | 
			
		||||
                // If the link is a (localised) matrix.to link, replace it with a pill
 | 
			
		||||
                const Pill = sdk.getComponent('elements.Pill');
 | 
			
		||||
                const Quote = sdk.getComponent('elements.Quote');
 | 
			
		||||
                if (Pill.isMessagePillUrl(href)) {
 | 
			
		||||
                    const pillContainer = document.createElement('span');
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -205,21 +201,6 @@ module.exports = React.createClass({
 | 
			
		|||
 | 
			
		||||
                    // update the current node with one that's now taken its place
 | 
			
		||||
                    node = pillContainer;
 | 
			
		||||
                } else if (SettingsStore.isFeatureEnabled("feature_rich_quoting") && Quote.isMessageUrl(href)) {
 | 
			
		||||
                    if (this.context.addRichQuote) { // We're already a Rich Quote so just append the next one above
 | 
			
		||||
                        this.context.addRichQuote(href);
 | 
			
		||||
                        node.remove();
 | 
			
		||||
                    } else { // We're the first in the chain
 | 
			
		||||
                        const quoteContainer = document.createElement('span');
 | 
			
		||||
 | 
			
		||||
                        const quote =
 | 
			
		||||
                            <Quote url={href} parentEv={this.props.mxEvent} node={node} />;
 | 
			
		||||
 | 
			
		||||
                        ReactDOM.render(quote, quoteContainer);
 | 
			
		||||
                        node.parentNode.replaceChild(quoteContainer, node);
 | 
			
		||||
                        node = quoteContainer;
 | 
			
		||||
                    }
 | 
			
		||||
                    pillified = true;
 | 
			
		||||
                }
 | 
			
		||||
            } else if (node.nodeType == Node.TEXT_NODE) {
 | 
			
		||||
                const Pill = sdk.getComponent('elements.Pill');
 | 
			
		||||
| 
						 | 
				
			
			@ -441,8 +422,12 @@ module.exports = React.createClass({
 | 
			
		|||
        const mxEvent = this.props.mxEvent;
 | 
			
		||||
        const content = mxEvent.getContent();
 | 
			
		||||
 | 
			
		||||
        const stripReply = SettingsStore.isFeatureEnabled("feature_rich_quoting") &&
 | 
			
		||||
            ReplyThread.getParentEventId(mxEvent);
 | 
			
		||||
        let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
 | 
			
		||||
            disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
 | 
			
		||||
            // Part of Replies fallback support
 | 
			
		||||
            stripReplyFallback: stripReply,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (this.props.highlightLink) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,8 @@ limitations under the License.
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import ReplyThread from "../elements/ReplyThread";
 | 
			
		||||
 | 
			
		||||
const React = require('react');
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
const classNames = require("classnames");
 | 
			
		||||
| 
						 | 
				
			
			@ -153,6 +155,11 @@ module.exports = withMatrixClient(React.createClass({
 | 
			
		|||
        isTwelveHour: PropTypes.bool,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    defaultProps: {
 | 
			
		||||
        // no-op function because onWidgetLoad is optional yet some subcomponents assume its existence
 | 
			
		||||
        onWidgetLoad: function() {},
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getInitialState: function() {
 | 
			
		||||
        return {
 | 
			
		||||
            // Whether the context menu is being displayed.
 | 
			
		||||
| 
						 | 
				
			
			@ -301,12 +308,16 @@ module.exports = withMatrixClient(React.createClass({
 | 
			
		|||
        const x = buttonRect.right + window.pageXOffset;
 | 
			
		||||
        const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
 | 
			
		||||
        const self = this;
 | 
			
		||||
 | 
			
		||||
        const {tile, replyThread} = this.refs;
 | 
			
		||||
 | 
			
		||||
        ContextualMenu.createMenu(MessageContextMenu, {
 | 
			
		||||
            chevronOffset: 10,
 | 
			
		||||
            mxEvent: this.props.mxEvent,
 | 
			
		||||
            left: x,
 | 
			
		||||
            top: y,
 | 
			
		||||
            eventTileOps: this.refs.tile && this.refs.tile.getEventTileOps ? this.refs.tile.getEventTileOps() : undefined,
 | 
			
		||||
            eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
 | 
			
		||||
            collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
 | 
			
		||||
            onFinished: function() {
 | 
			
		||||
                self.setState({menu: false});
 | 
			
		||||
            },
 | 
			
		||||
| 
						 | 
				
			
			@ -543,7 +554,7 @@ module.exports = withMatrixClient(React.createClass({
 | 
			
		|||
 | 
			
		||||
        if (needsSenderProfile) {
 | 
			
		||||
            let text = null;
 | 
			
		||||
            if (!this.props.tileShape || this.props.tileShape === 'quote') {
 | 
			
		||||
            if (!this.props.tileShape) {
 | 
			
		||||
                if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
 | 
			
		||||
                else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
 | 
			
		||||
                else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
 | 
			
		||||
| 
						 | 
				
			
			@ -647,18 +658,23 @@ module.exports = withMatrixClient(React.createClass({
 | 
			
		|||
                    </div>
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            case 'quote': {
 | 
			
		||||
 | 
			
		||||
            case 'reply':
 | 
			
		||||
            case 'reply_preview': {
 | 
			
		||||
                return (
 | 
			
		||||
                    <div className={classes}>
 | 
			
		||||
                        { avatar }
 | 
			
		||||
                        { sender }
 | 
			
		||||
                        <div className="mx_EventTile_line mx_EventTile_quote">
 | 
			
		||||
                        <div className="mx_EventTile_reply">
 | 
			
		||||
                            <a href={permalink} onClick={this.onPermalinkClicked}>
 | 
			
		||||
                                { timestamp }
 | 
			
		||||
                            </a>
 | 
			
		||||
                            { this._renderE2EPadlock() }
 | 
			
		||||
                            {
 | 
			
		||||
                                this.props.tileShape === 'reply_preview'
 | 
			
		||||
                                && ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread')
 | 
			
		||||
                            }
 | 
			
		||||
                            <EventTileType ref="tile"
 | 
			
		||||
                                           tileShape="quote"
 | 
			
		||||
                                           mxEvent={this.props.mxEvent}
 | 
			
		||||
                                           highlights={this.props.highlights}
 | 
			
		||||
                                           highlightLink={this.props.highlightLink}
 | 
			
		||||
| 
						 | 
				
			
			@ -681,6 +697,7 @@ module.exports = withMatrixClient(React.createClass({
 | 
			
		|||
                                { timestamp }
 | 
			
		||||
                            </a>
 | 
			
		||||
                            { this._renderE2EPadlock() }
 | 
			
		||||
                            { ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread') }
 | 
			
		||||
                            <EventTileType ref="tile"
 | 
			
		||||
                                           mxEvent={this.props.mxEvent}
 | 
			
		||||
                                           highlights={this.props.highlights}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -111,6 +111,14 @@ export default class MessageComposer extends React.Component {
 | 
			
		|||
            </li>);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
 | 
			
		||||
        let replyToWarning = null;
 | 
			
		||||
        if (isQuoting) {
 | 
			
		||||
            replyToWarning = <p>{
 | 
			
		||||
                _t('At this time it is not possible to reply with a file so this will be sent without being a reply.')
 | 
			
		||||
            }</p>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
 | 
			
		||||
            title: _t('Upload Files'),
 | 
			
		||||
            description: (
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +127,7 @@ export default class MessageComposer extends React.Component {
 | 
			
		|||
                    <ul style={{listStyle: 'none', textAlign: 'left'}}>
 | 
			
		||||
                        { fileList }
 | 
			
		||||
                    </ul>
 | 
			
		||||
                    { replyToWarning }
 | 
			
		||||
                </div>
 | 
			
		||||
            ),
 | 
			
		||||
            onFinished: (shouldUpload) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,9 +51,11 @@ const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g')
 | 
			
		|||
 | 
			
		||||
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
 | 
			
		||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
 | 
			
		||||
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
 | 
			
		||||
import QuotePreview from "./QuotePreview";
 | 
			
		||||
import {makeUserPermalink} from "../../../matrix-to";
 | 
			
		||||
import ReplyPreview from "./ReplyPreview";
 | 
			
		||||
import RoomViewStore from '../../../stores/RoomViewStore';
 | 
			
		||||
import ReplyThread from "../elements/ReplyThread";
 | 
			
		||||
import {ContentHelpers} from 'matrix-js-sdk';
 | 
			
		||||
 | 
			
		||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
 | 
			
		||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
 | 
			
		||||
| 
						 | 
				
			
			@ -273,7 +275,7 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
        let contentState = this.state.editorState.getCurrentContent();
 | 
			
		||||
 | 
			
		||||
        switch (payload.action) {
 | 
			
		||||
            case 'quote_event':
 | 
			
		||||
            case 'reply_to_event':
 | 
			
		||||
            case 'focus_composer':
 | 
			
		||||
                editor.focus();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			@ -751,16 +753,14 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const quotingEv = RoomViewStore.getQuotingEvent();
 | 
			
		||||
        const replyingToEv = RoomViewStore.getQuotingEvent();
 | 
			
		||||
        const mustSendHTML = Boolean(replyingToEv);
 | 
			
		||||
 | 
			
		||||
        if (this.state.isRichtextEnabled) {
 | 
			
		||||
            // We should only send HTML if any block is styled or contains inline style
 | 
			
		||||
            let shouldSendHTML = false;
 | 
			
		||||
 | 
			
		||||
            // If we are quoting we need HTML Content
 | 
			
		||||
            if (quotingEv) {
 | 
			
		||||
                shouldSendHTML = true;
 | 
			
		||||
            }
 | 
			
		||||
            if (mustSendHTML) shouldSendHTML = true;
 | 
			
		||||
 | 
			
		||||
            const blocks = contentState.getBlocksAsArray();
 | 
			
		||||
            if (blocks.some((block) => block.getType() !== 'unstyled')) {
 | 
			
		||||
| 
						 | 
				
			
			@ -820,15 +820,15 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
 | 
			
		||||
            const md = new Markdown(pt);
 | 
			
		||||
            // if contains no HTML and we're not quoting (needing HTML)
 | 
			
		||||
            if (md.isPlainText() && !quotingEv) {
 | 
			
		||||
            if (md.isPlainText() && !mustSendHTML) {
 | 
			
		||||
                contentText = md.toPlaintext();
 | 
			
		||||
            } else {
 | 
			
		||||
                contentHTML = md.toHTML();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let sendHtmlFn = this.client.sendHtmlMessage;
 | 
			
		||||
        let sendTextFn = this.client.sendTextMessage;
 | 
			
		||||
        let sendHtmlFn = ContentHelpers.makeHtmlMessage;
 | 
			
		||||
        let sendTextFn = ContentHelpers.makeTextMessage;
 | 
			
		||||
 | 
			
		||||
        this.historyManager.save(
 | 
			
		||||
            contentState,
 | 
			
		||||
| 
						 | 
				
			
			@ -836,45 +836,54 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
        );
 | 
			
		||||
 | 
			
		||||
        if (contentText.startsWith('/me')) {
 | 
			
		||||
            if (replyingToEv) {
 | 
			
		||||
                const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
			
		||||
                Modal.createTrackedDialog('Emote Reply Fail', '', ErrorDialog, {
 | 
			
		||||
                    title: _t("Unable to reply"),
 | 
			
		||||
                    description: _t("At this time it is not possible to reply with an emote."),
 | 
			
		||||
                });
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            contentText = contentText.substring(4);
 | 
			
		||||
            // bit of a hack, but the alternative would be quite complicated
 | 
			
		||||
            if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
 | 
			
		||||
            sendHtmlFn = this.client.sendHtmlEmote;
 | 
			
		||||
            sendTextFn = this.client.sendEmoteMessage;
 | 
			
		||||
            sendHtmlFn = ContentHelpers.makeHtmlEmote;
 | 
			
		||||
            sendTextFn = ContentHelpers.makeEmoteMessage;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (quotingEv) {
 | 
			
		||||
            const cli = MatrixClientPeg.get();
 | 
			
		||||
            const room = cli.getRoom(quotingEv.getRoomId());
 | 
			
		||||
            const sender = room.currentState.getMember(quotingEv.getSender());
 | 
			
		||||
 | 
			
		||||
            const {body/*, formatted_body*/} = quotingEv.getContent();
 | 
			
		||||
        let content = contentHTML ? sendHtmlFn(contentText, contentHTML) : sendTextFn(contentText);
 | 
			
		||||
 | 
			
		||||
            const perma = makeEventPermalink(quotingEv.getRoomId(), quotingEv.getId());
 | 
			
		||||
            contentText = `${sender.name}:\n> ${body}\n\n${contentText}`;
 | 
			
		||||
            contentHTML = `<a href="${perma}">Quote<br></a>${contentHTML}`;
 | 
			
		||||
        if (replyingToEv) {
 | 
			
		||||
            const replyContent = ReplyThread.makeReplyMixIn(replyingToEv);
 | 
			
		||||
            content = Object.assign(replyContent, content);
 | 
			
		||||
 | 
			
		||||
            // we have finished quoting, clear the quotingEvent
 | 
			
		||||
            // Part of Replies fallback support - prepend the text we're sending with the text we're replying to
 | 
			
		||||
            const nestedReply = ReplyThread.getNestedReplyText(replyingToEv);
 | 
			
		||||
            if (nestedReply) {
 | 
			
		||||
                if (content.formatted_body) {
 | 
			
		||||
                    content.formatted_body = nestedReply.html + content.formatted_body;
 | 
			
		||||
                }
 | 
			
		||||
                content.body = nestedReply.body + content.body;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Clear reply_to_event as we put the message into the queue
 | 
			
		||||
            // if the send fails, retry will handle resending.
 | 
			
		||||
            dis.dispatch({
 | 
			
		||||
                action: 'quote_event',
 | 
			
		||||
                action: 'reply_to_event',
 | 
			
		||||
                event: null,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let sendMessagePromise;
 | 
			
		||||
        if (contentHTML) {
 | 
			
		||||
            sendMessagePromise = sendHtmlFn.call(
 | 
			
		||||
                this.client, this.props.room.roomId, contentText, contentHTML,
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sendMessagePromise.done((res) => {
 | 
			
		||||
        this.client.sendMessage(this.props.room.roomId, content).then((res) => {
 | 
			
		||||
            dis.dispatch({
 | 
			
		||||
                action: 'message_sent',
 | 
			
		||||
            });
 | 
			
		||||
        }, (e) => onSendMessageFailed(e, this.props.room));
 | 
			
		||||
        }).catch((e) => {
 | 
			
		||||
            onSendMessageFailed(e, this.props.room);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.setState({
 | 
			
		||||
            editorState: this.createEditorState(),
 | 
			
		||||
| 
						 | 
				
			
			@ -1173,7 +1182,7 @@ export default class MessageComposerInput extends React.Component {
 | 
			
		|||
        return (
 | 
			
		||||
            <div className="mx_MessageComposer_input_wrapper">
 | 
			
		||||
                <div className="mx_MessageComposer_autocomplete_wrapper">
 | 
			
		||||
                    { SettingsStore.isFeatureEnabled("feature_rich_quoting") && <QuotePreview /> }
 | 
			
		||||
                    { SettingsStore.isFeatureEnabled("feature_rich_quoting") && <ReplyPreview /> }
 | 
			
		||||
                    <Autocomplete
 | 
			
		||||
                        ref={(e) => this.autocomplete = e}
 | 
			
		||||
                        room={this.props.room}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,15 +19,16 @@ import dis from '../../../dispatcher';
 | 
			
		|||
import sdk from '../../../index';
 | 
			
		||||
import { _t } from '../../../languageHandler';
 | 
			
		||||
import RoomViewStore from '../../../stores/RoomViewStore';
 | 
			
		||||
import SettingsStore from "../../../settings/SettingsStore";
 | 
			
		||||
 | 
			
		||||
function cancelQuoting() {
 | 
			
		||||
    dis.dispatch({
 | 
			
		||||
        action: 'quote_event',
 | 
			
		||||
        action: 'reply_to_event',
 | 
			
		||||
        event: null,
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class QuotePreview extends React.Component {
 | 
			
		||||
export default class ReplyPreview extends React.Component {
 | 
			
		||||
    constructor(props, context) {
 | 
			
		||||
        super(props, context);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,17 +62,20 @@ export default class QuotePreview extends React.Component {
 | 
			
		|||
        const EventTile = sdk.getComponent('rooms.EventTile');
 | 
			
		||||
        const EmojiText = sdk.getComponent('views.elements.EmojiText');
 | 
			
		||||
 | 
			
		||||
        return <div className="mx_QuotePreview">
 | 
			
		||||
            <div className="mx_QuotePreview_section">
 | 
			
		||||
                <EmojiText element="div" className="mx_QuotePreview_header mx_QuotePreview_title">
 | 
			
		||||
        return <div className="mx_ReplyPreview">
 | 
			
		||||
            <div className="mx_ReplyPreview_section">
 | 
			
		||||
                <EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
 | 
			
		||||
                    { '💬 ' + _t('Replying') }
 | 
			
		||||
                </EmojiText>
 | 
			
		||||
                <div className="mx_QuotePreview_header mx_QuotePreview_cancel">
 | 
			
		||||
                <div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
 | 
			
		||||
                    <img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
 | 
			
		||||
                         onClick={cancelQuoting} />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="mx_QuotePreview_clear" />
 | 
			
		||||
                <EventTile mxEvent={this.state.event} last={true} tileShape="quote" />
 | 
			
		||||
                <div className="mx_ReplyPreview_clear" />
 | 
			
		||||
                <EventTile last={true}
 | 
			
		||||
                           tileShape="reply_preview"
 | 
			
		||||
                           mxEvent={this.state.event}
 | 
			
		||||
                           isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -359,6 +359,7 @@
 | 
			
		|||
    "Filter room members": "Filter room members",
 | 
			
		||||
    "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
 | 
			
		||||
    "Attachment": "Attachment",
 | 
			
		||||
    "At this time it is not possible to reply with a file so this will be sent without being a reply.": "At this time it is not possible to reply with a file so this will be sent without being a reply.",
 | 
			
		||||
    "Upload Files": "Upload Files",
 | 
			
		||||
    "Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?",
 | 
			
		||||
    "Encrypted room": "Encrypted room",
 | 
			
		||||
| 
						 | 
				
			
			@ -379,6 +380,8 @@
 | 
			
		|||
    "Server error": "Server error",
 | 
			
		||||
    "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
 | 
			
		||||
    "Command error": "Command error",
 | 
			
		||||
    "Unable to reply": "Unable to reply",
 | 
			
		||||
    "At this time it is not possible to reply with an emote.": "At this time it is not possible to reply with an emote.",
 | 
			
		||||
    "bold": "bold",
 | 
			
		||||
    "italic": "italic",
 | 
			
		||||
    "strike": "strike",
 | 
			
		||||
| 
						 | 
				
			
			@ -406,9 +409,9 @@
 | 
			
		|||
    "Idle": "Idle",
 | 
			
		||||
    "Offline": "Offline",
 | 
			
		||||
    "Unknown": "Unknown",
 | 
			
		||||
    "Replying": "Replying",
 | 
			
		||||
    "Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
 | 
			
		||||
    "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
 | 
			
		||||
    "Replying": "Replying",
 | 
			
		||||
    "No rooms to show": "No rooms to show",
 | 
			
		||||
    "Unnamed room": "Unnamed room",
 | 
			
		||||
    "World readable": "World readable",
 | 
			
		||||
| 
						 | 
				
			
			@ -729,6 +732,7 @@
 | 
			
		|||
    "expand": "expand",
 | 
			
		||||
    "Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
 | 
			
		||||
    "Custom level": "Custom level",
 | 
			
		||||
    "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
 | 
			
		||||
    "<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
 | 
			
		||||
    "Room directory": "Room directory",
 | 
			
		||||
    "Start chat": "Start chat",
 | 
			
		||||
| 
						 | 
				
			
			@ -862,6 +866,7 @@
 | 
			
		|||
    "Permalink": "Permalink",
 | 
			
		||||
    "Quote": "Quote",
 | 
			
		||||
    "Source URL": "Source URL",
 | 
			
		||||
    "Collapse Reply Thread": "Collapse Reply Thread",
 | 
			
		||||
    "Failed to set Direct Message status of room": "Failed to set Direct Message status of room",
 | 
			
		||||
    "All messages (noisy)": "All messages (noisy)",
 | 
			
		||||
    "All messages": "All messages",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -111,10 +111,11 @@ class RoomViewStore extends Store {
 | 
			
		|||
                    forwardingEvent: payload.event,
 | 
			
		||||
                });
 | 
			
		||||
                break;
 | 
			
		||||
            case 'quote_event':
 | 
			
		||||
            case 'reply_to_event':
 | 
			
		||||
                this._setState({
 | 
			
		||||
                    quotingEvent: payload.event,
 | 
			
		||||
                    replyingToEvent: payload.event,
 | 
			
		||||
                });
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -132,8 +133,8 @@ class RoomViewStore extends Store {
 | 
			
		|||
                shouldPeek: payload.should_peek === undefined ? true : payload.should_peek,
 | 
			
		||||
                // have we sent a join request for this room and are waiting for a response?
 | 
			
		||||
                joining: payload.joining || false,
 | 
			
		||||
                // Reset quotingEvent because we don't want cross-room because bad UX
 | 
			
		||||
                quotingEvent: null,
 | 
			
		||||
                // Reset replyingToEvent because we don't want cross-room because bad UX
 | 
			
		||||
                replyingToEvent: null,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (this._state.forwardingEvent) {
 | 
			
		||||
| 
						 | 
				
			
			@ -297,7 +298,7 @@ class RoomViewStore extends Store {
 | 
			
		|||
 | 
			
		||||
    // The mxEvent if one is currently being replied to/quoted
 | 
			
		||||
    getQuotingEvent() {
 | 
			
		||||
        return this._state.quotingEvent;
 | 
			
		||||
        return this._state.replyingToEvent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    shouldPeek() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,39 +75,37 @@ describe('MessageComposerInput', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not send messages when composer is empty', () => {
 | 
			
		||||
        const textSpy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const htmlSpy = sinon.spy(client, 'sendHtmlMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(true);
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(textSpy.calledOnce).toEqual(false, 'should not send text message');
 | 
			
		||||
        expect(htmlSpy.calledOnce).toEqual(false, 'should not send html message');
 | 
			
		||||
        expect(spy.calledOnce).toEqual(false, 'should not send message');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not change content unnecessarily on RTE -> Markdown conversion', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(true);
 | 
			
		||||
        addTextToDraft('a');
 | 
			
		||||
        mci.handleKeyCommand('toggle-mode');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('a');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('a');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not change content unnecessarily on Markdown -> RTE conversion', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('a');
 | 
			
		||||
        mci.handleKeyCommand('toggle-mode');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('a');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('a');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should send emoji messages when rich text is enabled', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(true);
 | 
			
		||||
        addTextToDraft('☹');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +114,7 @@ describe('MessageComposerInput', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should send emoji messages when Markdown is enabled', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('☹');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
| 
						 | 
				
			
			@ -149,98 +147,98 @@ describe('MessageComposerInput', () => {
 | 
			
		|||
    // });
 | 
			
		||||
 | 
			
		||||
    it('should insert formatting characters in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        mci.handleKeyCommand('italic');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
        expect(['__', '**']).toContain(spy.args[0][1]);
 | 
			
		||||
        expect(['__', '**']).toContain(spy.args[0][1].body);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not entity-encode " in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('"');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('"');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('"');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should escape characters without other markup in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('\\*escaped\\*');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('*escaped*');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('*escaped*');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should escape characters with other markup in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendHtmlMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('\\*escaped\\* *italic*');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('\\*escaped\\* *italic*');
 | 
			
		||||
        expect(spy.args[0][2]).toEqual('*escaped* <em>italic</em>');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('\\*escaped\\* *italic*');
 | 
			
		||||
        expect(spy.args[0][1].formatted_body).toEqual('*escaped* <em>italic</em>');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not convert -_- into a horizontal rule in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('-_-');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('-_-');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('-_-');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not strip <del> tags in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendHtmlMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('<del>striked-out</del>');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('<del>striked-out</del>');
 | 
			
		||||
        expect(spy.args[0][2]).toEqual('<del>striked-out</del>');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('<del>striked-out</del>');
 | 
			
		||||
        expect(spy.args[0][1].formatted_body).toEqual('<del>striked-out</del>');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not strike-through ~~~ in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('~~~striked-out~~~');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('~~~striked-out~~~');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('~~~striked-out~~~');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not mark single unmarkedup paragraphs as HTML in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.calledOnce).toEqual(true);
 | 
			
		||||
        expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not mark two unmarkedup paragraphs as HTML in Markdown mode', () => {
 | 
			
		||||
        const spy = sinon.spy(client, 'sendTextMessage');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        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].body).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');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(false);
 | 
			
		||||
        mci.setDisplayedCompletion({
 | 
			
		||||
            completion: 'Some Member',
 | 
			
		||||
| 
						 | 
				
			
			@ -250,11 +248,11 @@ describe('MessageComposerInput', () => {
 | 
			
		|||
 | 
			
		||||
        mci.handleReturn(sinon.stub());
 | 
			
		||||
 | 
			
		||||
        expect(spy.args[0][1]).toEqual(
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual(
 | 
			
		||||
            'Some Member',
 | 
			
		||||
            'the plaintext body should only include the display name',
 | 
			
		||||
        );
 | 
			
		||||
        expect(spy.args[0][2]).toEqual(
 | 
			
		||||
        expect(spy.args[0][1].formatted_body).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',
 | 
			
		||||
        );
 | 
			
		||||
| 
						 | 
				
			
			@ -262,7 +260,7 @@ describe('MessageComposerInput', () => {
 | 
			
		|||
 | 
			
		||||
    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');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        mci.enableRichtext(true);
 | 
			
		||||
        mci.setDisplayedCompletion({
 | 
			
		||||
            completion: 'Some Member',
 | 
			
		||||
| 
						 | 
				
			
			@ -272,33 +270,33 @@ describe('MessageComposerInput', () => {
 | 
			
		|||
 | 
			
		||||
        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>');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('Some Member');
 | 
			
		||||
        expect(spy.args[0][1].formatted_body).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');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        // 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>');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
 | 
			
		||||
        expect(spy.args[0][1].formatted_body).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');
 | 
			
		||||
        const spy = sinon.spy(client, 'sendMessage');
 | 
			
		||||
        // 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>');
 | 
			
		||||
        expect(spy.args[0][1].body).toEqual('[Click here](https://some.lovely.url)');
 | 
			
		||||
        expect(spy.args[0][1].formatted_body).toEqual('<a href="https://some.lovely.url">Click here</a>');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,8 +95,7 @@ export function createTestClient() {
 | 
			
		|||
        mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
 | 
			
		||||
        setAccountData: sinon.stub(),
 | 
			
		||||
        sendTyping: sinon.stub().returns(Promise.resolve({})),
 | 
			
		||||
        sendTextMessage: () => Promise.resolve({}),
 | 
			
		||||
        sendHtmlMessage: () => Promise.resolve({}),
 | 
			
		||||
        sendMessage: () => Promise.resolve({}),
 | 
			
		||||
        getSyncState: () => "SYNCING",
 | 
			
		||||
        generateClientSecret: () => "t35tcl1Ent5ECr3T",
 | 
			
		||||
        isGuest: () => false,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue