commit
6bf1eb105a
19 changed files with 584 additions and 383 deletions
|
@ -53,7 +53,7 @@
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
@import "./views/elements/_MemberEventListSummary.scss";
|
@import "./views/elements/_MemberEventListSummary.scss";
|
||||||
@import "./views/elements/_ProgressBar.scss";
|
@import "./views/elements/_ProgressBar.scss";
|
||||||
@import "./views/elements/_Quote.scss";
|
@import "./views/elements/_ReplyThread.scss";
|
||||||
@import "./views/elements/_RichText.scss";
|
@import "./views/elements/_RichText.scss";
|
||||||
@import "./views/elements/_RoleButton.scss";
|
@import "./views/elements/_RoleButton.scss";
|
||||||
@import "./views/elements/_Spinner.scss";
|
@import "./views/elements/_Spinner.scss";
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||||
@import "./views/rooms/_PresenceLabel.scss";
|
@import "./views/rooms/_PresenceLabel.scss";
|
||||||
@import "./views/rooms/_QuotePreview.scss";
|
@import "./views/rooms/_ReplyPreview.scss";
|
||||||
@import "./views/rooms/_RoomDropTarget.scss";
|
@import "./views/rooms/_RoomDropTarget.scss";
|
||||||
@import "./views/rooms/_RoomHeader.scss";
|
@import "./views/rooms/_RoomHeader.scss";
|
||||||
@import "./views/rooms/_RoomList.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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_Quote .mx_DateSeparator {
|
.mx_ReplyThread .mx_DateSeparator {
|
||||||
font-size: 1em !important;
|
font-size: 1em !important;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Quote_show {
|
.mx_ReplyThread_show {
|
||||||
cursor: pointer;
|
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;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
position: relative;
|
position: relative;
|
||||||
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
|
||||||
margin-right: 110px;
|
margin-right: 110px;
|
||||||
|
@ -96,7 +96,7 @@ limitations under the License.
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_quote {
|
.mx_EventTile_reply {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ limitations under the License.
|
||||||
background-color: $event-selected-color;
|
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
|
.mx_EventTile.menu .mx_EventTile_line
|
||||||
{
|
{
|
||||||
background-color: $event-selected-color;
|
background-color: $event-selected-color;
|
||||||
|
@ -157,7 +157,8 @@ limitations under the License.
|
||||||
color: $event-notsent-color;
|
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;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
@ -202,10 +203,10 @@ limitations under the License.
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_last .mx_MessageTimestamp,
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
.mx_EventTile:hover .mx_MessageTimestamp,
|
.mx_EventTile_last > div > a > .mx_MessageTimestamp,
|
||||||
.mx_EventTile.menu .mx_MessageTimestamp
|
.mx_EventTile:hover > div > a > .mx_MessageTimestamp,
|
||||||
{
|
.mx_EventTile.menu > div > a > .mx_MessageTimestamp {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,12 +236,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover .mx_EventTile_editButton,
|
.mx_EventTile:hover .mx_EventTile_editButton,
|
||||||
.mx_EventTile.menu .mx_EventTile_editButton
|
.mx_EventTile.menu .mx_EventTile_editButton {
|
||||||
{
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile.menu .mx_MessageTimestamp {
|
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,8 +354,9 @@ limitations under the License.
|
||||||
border-left: $e2e-unverified-color 5px solid;
|
border-left: $e2e-unverified-color 5px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_MessageTimestamp,
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_MessageTimestamp {
|
.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;
|
left: 3px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
@ -370,8 +367,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_e2eIcon,
|
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
|
||||||
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_e2eIcon {
|
.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;
|
display: block;
|
||||||
left: 41px;
|
left: 41px;
|
||||||
}
|
}
|
||||||
|
@ -466,7 +464,7 @@ limitations under the License.
|
||||||
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
|
@ -484,7 +482,7 @@ limitations under the License.
|
||||||
.mx_EventTile_avatar {
|
.mx_EventTile_avatar {
|
||||||
top: 2px;
|
top: 2px;
|
||||||
}
|
}
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
@ -492,13 +490,13 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
|
.mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 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;
|
|
||||||
}
|
|
52
res/css/views/rooms/_ReplyPreview.scss
Normal file
52
res/css/views/rooms/_ReplyPreview.scss
Normal file
|
@ -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';
|
'use strict';
|
||||||
|
|
||||||
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const sanitizeHtml = require('sanitize-html');
|
const sanitizeHtml = require('sanitize-html');
|
||||||
const highlight = require('highlight.js');
|
const highlight = require('highlight.js');
|
||||||
|
@ -184,6 +186,7 @@ const sanitizeHtmlParams = {
|
||||||
],
|
],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
// custom ones first:
|
// 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
|
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
|
span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
|
||||||
a: ['href', 'name', 'target', 'rel'], // remote target: 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.highlightLink: optional href to add to highlighted words
|
||||||
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
* 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={}) {
|
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 bodyHasEmoji = false;
|
||||||
|
|
||||||
|
let strippedBody;
|
||||||
let safeBody;
|
let safeBody;
|
||||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
// 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
|
// 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
|
// Only generate safeBody if the message was sent as org.matrix.custom.html
|
||||||
if (isHtml) {
|
if (isHtml) {
|
||||||
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
safeBody = sanitizeHtml(formattedBody, sanitizeHtmlParams);
|
||||||
} else {
|
} else {
|
||||||
// ... or if there are emoji, which we insert as HTML alongside the
|
// ... or if there are emoji, which we insert as HTML alongside the
|
||||||
// escaped plaintext body.
|
// escaped plaintext body.
|
||||||
if (bodyHasEmoji) {
|
if (bodyHasEmoji) {
|
||||||
isHtml = true;
|
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;
|
let emojiBody = false;
|
||||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
const contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
|
||||||
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||||
}
|
}
|
||||||
|
@ -471,7 +481,7 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
|
|
||||||
return isHtml ?
|
return isHtml ?
|
||||||
<span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
<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) {
|
export function emojifyText(text) {
|
||||||
|
|
|
@ -908,17 +908,17 @@ module.exports = React.createClass({
|
||||||
this.setState({ draggingFile: false });
|
this.setState({ draggingFile: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadFile: function(file) {
|
uploadFile: async function(file) {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'view_set_mxid'});
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentMessages.sendContentToRoom(
|
try {
|
||||||
file, this.state.room.roomId, MatrixClientPeg.get(),
|
await ContentMessages.sendContentToRoom(file, this.state.room.roomId, MatrixClientPeg.get());
|
||||||
).catch((error) => {
|
} catch (error) {
|
||||||
if (error.name === "UnknownDeviceError") {
|
if (error.name === "UnknownDeviceError") {
|
||||||
// Let the staus bar handle this
|
// Let the status bar handle this
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -928,6 +928,14 @@ module.exports = React.createClass({
|
||||||
description: ((error && error.message)
|
description: ((error && error.message)
|
||||||
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
? 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';
|
'use strict';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
@ -34,13 +35,16 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
/* the MatrixEvent associated with the context menu */
|
/* 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 */
|
/* 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 */
|
/* callback called when the menu is dismissed */
|
||||||
onFinished: React.PropTypes.func,
|
onFinished: PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -182,12 +186,17 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onReplyClick: function() {
|
onReplyClick: function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'quote_event',
|
action: 'reply_to_event',
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
});
|
});
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onCollapseReplyThreadClick: function() {
|
||||||
|
this.props.collapseReplyThread();
|
||||||
|
this.closeMenu();
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const eventStatus = this.props.mxEvent.status;
|
const eventStatus = this.props.mxEvent.status;
|
||||||
let resendButton;
|
let resendButton;
|
||||||
|
@ -200,6 +209,7 @@ module.exports = React.createClass({
|
||||||
let externalURLButton;
|
let externalURLButton;
|
||||||
let quoteButton;
|
let quoteButton;
|
||||||
let replyButton;
|
let replyButton;
|
||||||
|
let collapseReplyThread;
|
||||||
|
|
||||||
if (eventStatus === 'not_sent') {
|
if (eventStatus === 'not_sent') {
|
||||||
resendButton = (
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -320,6 +337,7 @@ module.exports = React.createClass({
|
||||||
{ quoteButton }
|
{ quoteButton }
|
||||||
{ replyButton }
|
{ replyButton }
|
||||||
{ externalURLButton }
|
{ externalURLButton }
|
||||||
|
{ collapseReplyThread }
|
||||||
</div>
|
</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;
|
|
||||||
}
|
|
||||||
}
|
|
306
src/components/views/elements/ReplyThread.js
Normal file
306
src/components/views/elements/ReplyThread.js
Normal file
|
@ -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 ContextualMenu from '../../structures/ContextualMenu';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||||
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -61,10 +62,6 @@ module.exports = React.createClass({
|
||||||
tileShape: PropTypes.string,
|
tileShape: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
addRichQuote: PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
// the URLs (if any) to be previewed with a LinkPreviewWidget
|
// 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
|
// If the link is a (localised) matrix.to link, replace it with a pill
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
const Quote = sdk.getComponent('elements.Quote');
|
|
||||||
if (Pill.isMessagePillUrl(href)) {
|
if (Pill.isMessagePillUrl(href)) {
|
||||||
const pillContainer = document.createElement('span');
|
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
|
// update the current node with one that's now taken its place
|
||||||
node = pillContainer;
|
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) {
|
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
|
@ -441,8 +422,12 @@ module.exports = React.createClass({
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
const content = mxEvent.getContent();
|
const content = mxEvent.getContent();
|
||||||
|
|
||||||
|
const stripReply = SettingsStore.isFeatureEnabled("feature_rich_quoting") &&
|
||||||
|
ReplyThread.getParentEventId(mxEvent);
|
||||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||||
|
// Part of Replies fallback support
|
||||||
|
stripReplyFallback: stripReply,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.props.highlightLink) {
|
if (this.props.highlightLink) {
|
||||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
const classNames = require("classnames");
|
const classNames = require("classnames");
|
||||||
|
@ -153,6 +155,11 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
isTwelveHour: PropTypes.bool,
|
isTwelveHour: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
defaultProps: {
|
||||||
|
// no-op function because onWidgetLoad is optional yet some subcomponents assume its existence
|
||||||
|
onWidgetLoad: function() {},
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
// Whether the context menu is being displayed.
|
// Whether the context menu is being displayed.
|
||||||
|
@ -301,12 +308,16 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const x = buttonRect.right + window.pageXOffset;
|
const x = buttonRect.right + window.pageXOffset;
|
||||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
const {tile, replyThread} = this.refs;
|
||||||
|
|
||||||
ContextualMenu.createMenu(MessageContextMenu, {
|
ContextualMenu.createMenu(MessageContextMenu, {
|
||||||
chevronOffset: 10,
|
chevronOffset: 10,
|
||||||
mxEvent: this.props.mxEvent,
|
mxEvent: this.props.mxEvent,
|
||||||
left: x,
|
left: x,
|
||||||
top: y,
|
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() {
|
onFinished: function() {
|
||||||
self.setState({menu: false});
|
self.setState({menu: false});
|
||||||
},
|
},
|
||||||
|
@ -543,7 +554,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
|
|
||||||
if (needsSenderProfile) {
|
if (needsSenderProfile) {
|
||||||
let text = null;
|
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');
|
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.video') text = _td('%(senderName)s sent a video');
|
||||||
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
|
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
|
||||||
|
@ -647,18 +658,23 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'quote': {
|
|
||||||
|
case 'reply':
|
||||||
|
case 'reply_preview': {
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{ avatar }
|
{ avatar }
|
||||||
{ sender }
|
{ sender }
|
||||||
<div className="mx_EventTile_line mx_EventTile_quote">
|
<div className="mx_EventTile_reply">
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ this._renderE2EPadlock() }
|
{ this._renderE2EPadlock() }
|
||||||
|
{
|
||||||
|
this.props.tileShape === 'reply_preview'
|
||||||
|
&& ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread')
|
||||||
|
}
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
tileShape="quote"
|
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
|
@ -681,6 +697,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ this._renderE2EPadlock() }
|
{ this._renderE2EPadlock() }
|
||||||
|
{ ReplyThread.makeThread(this.props.mxEvent, this.props.onWidgetLoad, 'replyThread') }
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
|
|
|
@ -111,6 +111,14 @@ export default class MessageComposer extends React.Component {
|
||||||
</li>);
|
</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, {
|
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
|
||||||
title: _t('Upload Files'),
|
title: _t('Upload Files'),
|
||||||
description: (
|
description: (
|
||||||
|
@ -119,6 +127,7 @@ export default class MessageComposer extends React.Component {
|
||||||
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
||||||
{ fileList }
|
{ fileList }
|
||||||
</ul>
|
</ul>
|
||||||
|
{ replyToWarning }
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
onFinished: (shouldUpload) => {
|
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 {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
|
import {makeUserPermalink} from "../../../matrix-to";
|
||||||
import QuotePreview from "./QuotePreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
import {ContentHelpers} from 'matrix-js-sdk';
|
||||||
|
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||||
|
@ -273,7 +275,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let contentState = this.state.editorState.getCurrentContent();
|
let contentState = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'quote_event':
|
case 'reply_to_event':
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
editor.focus();
|
editor.focus();
|
||||||
break;
|
break;
|
||||||
|
@ -751,16 +753,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quotingEv = RoomViewStore.getQuotingEvent();
|
const replyingToEv = RoomViewStore.getQuotingEvent();
|
||||||
|
const mustSendHTML = Boolean(replyingToEv);
|
||||||
|
|
||||||
if (this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
// We should only send HTML if any block is styled or contains inline style
|
// We should only send HTML if any block is styled or contains inline style
|
||||||
let shouldSendHTML = false;
|
let shouldSendHTML = false;
|
||||||
|
|
||||||
// If we are quoting we need HTML Content
|
if (mustSendHTML) shouldSendHTML = true;
|
||||||
if (quotingEv) {
|
|
||||||
shouldSendHTML = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blocks = contentState.getBlocksAsArray();
|
const blocks = contentState.getBlocksAsArray();
|
||||||
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
||||||
|
@ -820,15 +820,15 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
const md = new Markdown(pt);
|
const md = new Markdown(pt);
|
||||||
// if contains no HTML and we're not quoting (needing HTML)
|
// if contains no HTML and we're not quoting (needing HTML)
|
||||||
if (md.isPlainText() && !quotingEv) {
|
if (md.isPlainText() && !mustSendHTML) {
|
||||||
contentText = md.toPlaintext();
|
contentText = md.toPlaintext();
|
||||||
} else {
|
} else {
|
||||||
contentHTML = md.toHTML();
|
contentHTML = md.toHTML();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendHtmlFn = this.client.sendHtmlMessage;
|
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||||
let sendTextFn = this.client.sendTextMessage;
|
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||||
|
|
||||||
this.historyManager.save(
|
this.historyManager.save(
|
||||||
contentState,
|
contentState,
|
||||||
|
@ -836,45 +836,54 @@ export default class MessageComposerInput extends React.Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentText.startsWith('/me')) {
|
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);
|
contentText = contentText.substring(4);
|
||||||
// bit of a hack, but the alternative would be quite complicated
|
// bit of a hack, but the alternative would be quite complicated
|
||||||
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
|
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
|
||||||
sendHtmlFn = this.client.sendHtmlEmote;
|
sendHtmlFn = ContentHelpers.makeHtmlEmote;
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
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());
|
if (replyingToEv) {
|
||||||
contentText = `${sender.name}:\n> ${body}\n\n${contentText}`;
|
const replyContent = ReplyThread.makeReplyMixIn(replyingToEv);
|
||||||
contentHTML = `<a href="${perma}">Quote<br></a>${contentHTML}`;
|
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({
|
dis.dispatch({
|
||||||
action: 'quote_event',
|
action: 'reply_to_event',
|
||||||
event: null,
|
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({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
});
|
});
|
||||||
}, (e) => onSendMessageFailed(e, this.props.room));
|
}).catch((e) => {
|
||||||
|
onSendMessageFailed(e, this.props.room);
|
||||||
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(),
|
editorState: this.createEditorState(),
|
||||||
|
@ -1173,7 +1182,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer_input_wrapper">
|
<div className="mx_MessageComposer_input_wrapper">
|
||||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||||
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <QuotePreview /> }
|
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <ReplyPreview /> }
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
|
|
@ -19,15 +19,16 @@ import dis from '../../../dispatcher';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function cancelQuoting() {
|
function cancelQuoting() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'quote_event',
|
action: 'reply_to_event',
|
||||||
event: null,
|
event: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QuotePreview extends React.Component {
|
export default class ReplyPreview extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
@ -61,17 +62,20 @@ export default class QuotePreview extends React.Component {
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
return <div className="mx_QuotePreview">
|
return <div className="mx_ReplyPreview">
|
||||||
<div className="mx_QuotePreview_section">
|
<div className="mx_ReplyPreview_section">
|
||||||
<EmojiText element="div" className="mx_QuotePreview_header mx_QuotePreview_title">
|
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||||
{ '💬 ' + _t('Replying') }
|
{ '💬 ' + _t('Replying') }
|
||||||
</EmojiText>
|
</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"
|
<img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
||||||
onClick={cancelQuoting} />
|
onClick={cancelQuoting} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_QuotePreview_clear" />
|
<div className="mx_ReplyPreview_clear" />
|
||||||
<EventTile mxEvent={this.state.event} last={true} tileShape="quote" />
|
<EventTile last={true}
|
||||||
|
tileShape="reply_preview"
|
||||||
|
mxEvent={this.state.event}
|
||||||
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
|
@ -359,6 +359,7 @@
|
||||||
"Filter room members": "Filter room members",
|
"Filter room members": "Filter room members",
|
||||||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
||||||
"Attachment": "Attachment",
|
"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",
|
"Upload Files": "Upload Files",
|
||||||
"Are you sure you want to upload the following files?": "Are you sure you want to upload the following 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",
|
"Encrypted room": "Encrypted room",
|
||||||
|
@ -379,6 +380,8 @@
|
||||||
"Server error": "Server error",
|
"Server error": "Server error",
|
||||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||||
"Command error": "Command error",
|
"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",
|
"bold": "bold",
|
||||||
"italic": "italic",
|
"italic": "italic",
|
||||||
"strike": "strike",
|
"strike": "strike",
|
||||||
|
@ -406,9 +409,9 @@
|
||||||
"Idle": "Idle",
|
"Idle": "Idle",
|
||||||
"Offline": "Offline",
|
"Offline": "Offline",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Replying": "Replying",
|
|
||||||
"Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s",
|
"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",
|
"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",
|
"No rooms to show": "No rooms to show",
|
||||||
"Unnamed room": "Unnamed room",
|
"Unnamed room": "Unnamed room",
|
||||||
"World readable": "World readable",
|
"World readable": "World readable",
|
||||||
|
@ -729,6 +732,7 @@
|
||||||
"expand": "expand",
|
"expand": "expand",
|
||||||
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
||||||
"Custom level": "Custom level",
|
"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>",
|
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
|
||||||
"Room directory": "Room directory",
|
"Room directory": "Room directory",
|
||||||
"Start chat": "Start chat",
|
"Start chat": "Start chat",
|
||||||
|
@ -862,6 +866,7 @@
|
||||||
"Permalink": "Permalink",
|
"Permalink": "Permalink",
|
||||||
"Quote": "Quote",
|
"Quote": "Quote",
|
||||||
"Source URL": "Source URL",
|
"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",
|
"Failed to set Direct Message status of room": "Failed to set Direct Message status of room",
|
||||||
"All messages (noisy)": "All messages (noisy)",
|
"All messages (noisy)": "All messages (noisy)",
|
||||||
"All messages": "All messages",
|
"All messages": "All messages",
|
||||||
|
|
|
@ -111,10 +111,11 @@ class RoomViewStore extends Store {
|
||||||
forwardingEvent: payload.event,
|
forwardingEvent: payload.event,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'quote_event':
|
case 'reply_to_event':
|
||||||
this._setState({
|
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,
|
shouldPeek: payload.should_peek === undefined ? true : payload.should_peek,
|
||||||
// have we sent a join request for this room and are waiting for a response?
|
// have we sent a join request for this room and are waiting for a response?
|
||||||
joining: payload.joining || false,
|
joining: payload.joining || false,
|
||||||
// Reset quotingEvent because we don't want cross-room because bad UX
|
// Reset replyingToEvent because we don't want cross-room because bad UX
|
||||||
quotingEvent: null,
|
replyingToEvent: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this._state.forwardingEvent) {
|
if (this._state.forwardingEvent) {
|
||||||
|
@ -297,7 +298,7 @@ class RoomViewStore extends Store {
|
||||||
|
|
||||||
// The mxEvent if one is currently being replied to/quoted
|
// The mxEvent if one is currently being replied to/quoted
|
||||||
getQuotingEvent() {
|
getQuotingEvent() {
|
||||||
return this._state.quotingEvent;
|
return this._state.replyingToEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldPeek() {
|
shouldPeek() {
|
||||||
|
|
|
@ -75,39 +75,37 @@ describe('MessageComposerInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not send messages when composer is empty', () => {
|
it('should not send messages when composer is empty', () => {
|
||||||
const textSpy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
const htmlSpy = sinon.spy(client, 'sendHtmlMessage');
|
|
||||||
mci.enableRichtext(true);
|
mci.enableRichtext(true);
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(textSpy.calledOnce).toEqual(false, 'should not send text message');
|
expect(spy.calledOnce).toEqual(false, 'should not send message');
|
||||||
expect(htmlSpy.calledOnce).toEqual(false, 'should not send html message');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not change content unnecessarily on RTE -> Markdown conversion', () => {
|
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);
|
mci.enableRichtext(true);
|
||||||
addTextToDraft('a');
|
addTextToDraft('a');
|
||||||
mci.handleKeyCommand('toggle-mode');
|
mci.handleKeyCommand('toggle-mode');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
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', () => {
|
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);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('a');
|
addTextToDraft('a');
|
||||||
mci.handleKeyCommand('toggle-mode');
|
mci.handleKeyCommand('toggle-mode');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
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', () => {
|
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);
|
mci.enableRichtext(true);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
@ -116,7 +114,7 @@ describe('MessageComposerInput', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send emoji messages when Markdown is enabled', () => {
|
it('should send emoji messages when Markdown is enabled', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
@ -149,98 +147,98 @@ describe('MessageComposerInput', () => {
|
||||||
// });
|
// });
|
||||||
|
|
||||||
it('should insert formatting characters in Markdown mode', () => {
|
it('should insert formatting characters in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
mci.handleKeyCommand('italic');
|
mci.handleKeyCommand('italic');
|
||||||
mci.handleReturn(sinon.stub());
|
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', () => {
|
it('should not entity-encode " in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('"');
|
addTextToDraft('"');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
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', () => {
|
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);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('\\*escaped\\*');
|
addTextToDraft('\\*escaped\\*');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
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', () => {
|
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);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('\\*escaped\\* *italic*');
|
addTextToDraft('\\*escaped\\* *italic*');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('\\*escaped\\* *italic*');
|
expect(spy.args[0][1].body).toEqual('\\*escaped\\* *italic*');
|
||||||
expect(spy.args[0][2]).toEqual('*escaped* <em>italic</em>');
|
expect(spy.args[0][1].formatted_body).toEqual('*escaped* <em>italic</em>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not convert -_- into a horizontal rule in Markdown mode', () => {
|
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);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('-_-');
|
addTextToDraft('-_-');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
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', () => {
|
it('should not strip <del> tags in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendHtmlMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('<del>striked-out</del>');
|
addTextToDraft('<del>striked-out</del>');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('<del>striked-out</del>');
|
expect(spy.args[0][1].body).toEqual('<del>striked-out</del>');
|
||||||
expect(spy.args[0][2]).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', () => {
|
it('should not strike-through ~~~ in Markdown mode', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('~~~striked-out~~~');
|
addTextToDraft('~~~striked-out~~~');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
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', () => {
|
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);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
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', () => {
|
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);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.');
|
expect(spy.args[0][1].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', () => {
|
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)
|
// 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.enableRichtext(false);
|
||||||
mci.setDisplayedCompletion({
|
mci.setDisplayedCompletion({
|
||||||
completion: 'Some Member',
|
completion: 'Some Member',
|
||||||
|
@ -250,11 +248,11 @@ describe('MessageComposerInput', () => {
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.args[0][1]).toEqual(
|
expect(spy.args[0][1].body).toEqual(
|
||||||
'Some Member',
|
'Some Member',
|
||||||
'the plaintext body should only include the display name',
|
'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>',
|
'<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',
|
'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', () => {
|
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)
|
// 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.enableRichtext(true);
|
||||||
mci.setDisplayedCompletion({
|
mci.setDisplayedCompletion({
|
||||||
completion: 'Some Member',
|
completion: 'Some Member',
|
||||||
|
@ -272,33 +270,33 @@ describe('MessageComposerInput', () => {
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.args[0][1]).toEqual('Some Member');
|
expect(spy.args[0][1].body).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].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', () => {
|
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)
|
// 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
|
// Markdown mode enabled
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
addTextToDraft('[My Not-Tab-Completed Mention](https://matrix.to/#/@some_member:domain.bla)');
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
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][1].body).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].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', () => {
|
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)
|
// 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
|
// Markdown mode enabled
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('[Click here](https://some.lovely.url)');
|
addTextToDraft('[Click here](https://some.lovely.url)');
|
||||||
|
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.args[0][1]).toEqual('[Click here](https://some.lovely.url)');
|
expect(spy.args[0][1].body).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].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/',
|
mxcUrlToHttp: (mxc) => 'http://this.is.a.url/',
|
||||||
setAccountData: sinon.stub(),
|
setAccountData: sinon.stub(),
|
||||||
sendTyping: sinon.stub().returns(Promise.resolve({})),
|
sendTyping: sinon.stub().returns(Promise.resolve({})),
|
||||||
sendTextMessage: () => Promise.resolve({}),
|
sendMessage: () => Promise.resolve({}),
|
||||||
sendHtmlMessage: () => Promise.resolve({}),
|
|
||||||
getSyncState: () => "SYNCING",
|
getSyncState: () => "SYNCING",
|
||||||
generateClientSecret: () => "t35tcl1Ent5ECr3T",
|
generateClientSecret: () => "t35tcl1Ent5ECr3T",
|
||||||
isGuest: () => false,
|
isGuest: () => false,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue