From 0f853915876b10d34ed53f6365c30bb5f7d34ad4 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Sun, 10 Dec 2017 12:50:41 +0000
Subject: [PATCH] Implement Rich Quoting/Replies
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/components/views/elements/Quote.js | 138 +++++++++++++++
src/components/views/messages/TextualBody.js | 17 ++
src/components/views/rooms/EventTile.js | 159 ++++++++++--------
.../views/rooms/MessageComposerInput.js | 65 +++----
src/components/views/rooms/QuotePreview.js | 75 +++++++++
src/linkify-matrix.js | 4 +-
src/matrix-to.js | 33 ++++
src/stores/RoomViewStore.js | 12 ++
8 files changed, 405 insertions(+), 98 deletions(-)
create mode 100644 src/components/views/elements/Quote.js
create mode 100644 src/components/views/rooms/QuotePreview.js
create mode 100644 src/matrix-to.js
diff --git a/src/components/views/elements/Quote.js b/src/components/views/elements/Quote.js
new file mode 100644
index 0000000000..1925cfbd65
--- /dev/null
+++ b/src/components/views/elements/Quote.js
@@ -0,0 +1,138 @@
+/*
+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 dis from '../../../dispatcher';
+import classNames from 'classnames';
+import { Room, RoomMember } from 'matrix-js-sdk';
+import PropTypes from 'prop-types';
+import MatrixClientPeg from '../../../MatrixClientPeg';
+import { getDisplayAliasForRoom } from '../../../Rooms';
+import {makeUserPermalink} from "../../../matrix-to";
+import DateUtils from "../../../DateUtils";
+
+// 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\/(([\#\!])[^\/]*)\/(\$[^\/]*)$/;
+
+const Quote = React.createClass({
+ statics: {
+ isMessageUrl: (url) => {
+ return !!REGEX_LOCAL_MATRIXTO.exec(url);
+ },
+ },
+
+ childContextTypes: {
+ matrixClient: React.PropTypes.object,
+ },
+
+ props: {
+ // The matrix.to url of the event
+ url: PropTypes.string,
+ // Whether to include an avatar in the pill
+ shouldShowPillAvatar: PropTypes.bool,
+ },
+
+ getChildContext: function() {
+ return {
+ matrixClient: MatrixClientPeg.get(),
+ };
+ },
+
+ getInitialState() {
+ return {
+ // The room related to this quote
+ room: null,
+ // The event related to this quote
+ event: null,
+ };
+ },
+
+ componentWillReceiveProps(nextProps) {
+ let roomId;
+ let prefix;
+ let eventId;
+
+ if (nextProps.url) {
+ // 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(nextProps.url) || [];
+
+ roomId = matrixToMatch[1]; // The room ID
+ prefix = matrixToMatch[2]; // The first character of prefix
+ eventId = matrixToMatch[3]; // The event ID
+ }
+
+ const room = prefix === '#' ?
+ MatrixClientPeg.get().getRooms().find((r) => {
+ return r.getAliases().includes(roomId);
+ }) : MatrixClientPeg.get().getRoom(roomId);
+
+ // 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.
+ if (room) this.getEvent(room, eventId);
+ },
+
+ componentWillMount() {
+ this._unmounted = false;
+ this.componentWillReceiveProps(this.props);
+ },
+
+ componentWillUnmount() {
+ this._unmounted = true;
+ },
+
+ async getEvent(room, eventId) {
+ let event = room.findEventById(eventId);
+ if (event) {
+ this.setState({room, event});
+ return;
+ }
+
+ await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
+ event = room.findEventById(eventId);
+ this.setState({room, event});
+ },
+
+ render: function() {
+ const ev = this.state.event;
+ if (ev) {
+ const EventTile = sdk.getComponent('views.rooms.EventTile');
+ // const EmojiText = sdk.getComponent('views.elements.EmojiText');
+ // const Pill = sdk.getComponent('views.elements.Pill');
+ // const senderUrl = makeUserPermalink(ev.getSender());
+ // const EventTileType = sdk.getComponent(EventTile.getHandlerTile(ev));
+ /*return */
+ return
+ {/**/}
+ {/*
;
+ /*;*/
+ } else {
+ // Deliberately render nothing if the URL isn't recognised
+ return
; + + ReactDOM.render(quote, quoteContainer); + node.parentNode.replaceChild(quoteContainer, node); + + pillified = true; + + node = quoteContainer; } } else if (node.nodeType == Node.TEXT_NODE) { const Pill = sdk.getComponent('elements.Pill'); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 3407ea159d..645a3fdb71 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +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. @@ -528,79 +529,103 @@ module.exports = withMatrixClient(React.createClass({ const timestamp = this.props.mxEvent.getTs() ?
${body}`); - if (!this.state.isRichtextEnabled) { - content = ContentState.createFromText(RichText.stateToMarkdown(content)); - } - - const blockMap = content.getBlockMap(); - let startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey()); - contentState = Modifier.splitBlock(contentState, startSelection); - startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey()); - contentState = Modifier.replaceWithFragment(contentState, - startSelection, - blockMap); - startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey()); - if (this.state.isRichtextEnabled) { - contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote'); - } - let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); - editorState = EditorState.moveSelectionToEnd(editorState); - this.onEditorContentChanged(editorState); - editor.focus(); - } - } - break; } }; @@ -659,7 +634,7 @@ export default class MessageComposerInput extends React.Component { } return false; - } + }; onTextPasted(text: string, html?: string) { const currentSelection = this.state.editorState.getSelection(); @@ -749,9 +724,17 @@ export default class MessageComposerInput extends React.Component { return true; } + const quotingEv = RoomViewStore.getQuotingEvent(); + 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; + } + const blocks = contentState.getBlocksAsArray(); if (blocks.some((block) => block.getType() !== 'unstyled')) { shouldSendHTML = true; @@ -809,7 +792,8 @@ export default class MessageComposerInput extends React.Component { }).join('\n'); const md = new Markdown(pt); - if (md.isPlainText()) { + // if contains no HTML and we're not quoting (needing HTML) + if (md.isPlainText() && !quotingEv) { contentText = md.toPlaintext(); } else { contentHTML = md.toHTML(); @@ -832,6 +816,24 @@ export default class MessageComposerInput extends React.Component { sendTextFn = this.client.sendEmoteMessage; } + 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(); + + const perma = makeEventPermalink(quotingEv.getRoomId(), quotingEv.getId()); + contentText = `${sender.name}:\n> ${body}\n\n${contentText}`; + contentHTML = `Quote