From 1c3d8cbe6e27781b568753a39fd1a73c15d306f2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 10 Feb 2018 11:19:43 +0000 Subject: [PATCH] initial refactor of Replies to use `B` explicit over-the-wire format Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Quote.js | 230 +++++++++--------- src/components/views/messages/TextualBody.js | 19 -- src/components/views/rooms/EventTile.js | 6 +- .../views/rooms/MessageComposerInput.js | 54 ++-- src/components/views/rooms/QuotePreview.js | 4 +- 5 files changed, 140 insertions(+), 173 deletions(-) diff --git a/src/components/views/elements/Quote.js b/src/components/views/elements/Quote.js index 761f7aa151..fa571aa063 100644 --- a/src/components/views/elements/Quote.js +++ b/src/components/views/elements/Quote.js @@ -21,33 +21,20 @@ 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\/([\#\!][^\/]*)\/(\$[^\/]*)$/; +import SettingsStore from "../../../settings/SettingsStore"; 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), + + onWidgetLoad: PropTypes.func.isRequired, }; constructor(props, context) { super(props, context); + /* this.state = { // The event related to this quote and their nested rich quotes events: [], @@ -55,45 +42,54 @@ export default class Quote extends React.Component { show: true, // Whether an error was encountered fetching nested older event, show node if it does err: false, + loading: true, + };*/ + + 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: null, }; 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); + this.room = this.getRoom(this.props.parentEv.getRoomId()); + this.initialize(); + } + + async initialize() { + const {parentEv} = this.props; + const inReplyTo = Quote.getInReplyTo(parentEv); + + const ev = await this.getEvent(this.room, inReplyTo['event_id']); + this.setState({ + events: [ev], + }, this.loadNextEvent); + } + + async loadNextEvent() { + this.props.onWidgetLoad(); + const ev = this.state.events[0]; + const inReplyTo = Quote.getInReplyTo(ev); + + if (!inReplyTo) { + this.setState({ + loading: false, + }); + return; + } + + const loadedEv = await this.getEvent(this.room, inReplyTo['event_id']); + this.setState({loadedEv}); } getRoom(id) { @@ -105,84 +101,84 @@ export default class Quote extends React.Component { }); } - async getEvent(room, eventId, show) { + async getEvent(room, eventId) { const event = room.findEventById(eventId); - if (event) { - this.addEvent(event, show); - return; - } + if (event) return event; 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); + return room.findEventById(eventId); } onQuoteClick() { - this.setState({ show: true }); + const events = [this.state.loadedEv].concat(this.state.events); + + this.setState({ + loadedEv: null, + events, + }, this.loadNextEvent); + } + + static getInReplyTo(ev) { + if (ev.isRedacted()) return; + + const {'m.relates_to': mRelatesTo} = ev.getContent(); + if (mRelatesTo) { + return mRelatesTo['m.in_reply_to']; + } + } + + static getRelationship(ev) { + return { + 'm.relates_to': { + 'm.in_reply_to': { + 'event_id': ev.getId(), + }, + }, + }; + } + + static getQuote(parentEv, onWidgetLoad) { + if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !Quote.getInReplyTo(parentEv)) return null; + return ; } 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(
- { - _t('In reply to ', {}, { - 'a': (sub) => { sub }, - 'pill': , - }) - } -
); - } - - 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 = ; + let header = null; + if (this.state.loadedEv) { + const ev = this.state.loadedEv; + const Pill = sdk.getComponent('elements.Pill'); + const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); + header =
+ { + _t('In reply to ', {}, { + 'a': (sub) => { sub }, + 'pill': , + }) } - - evTiles.push(
- { dateSep } - -
); - }); - - return
{ evTiles }
; +
; + } else if (this.state.loading) { + header =
LOADING...
; } - // 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; + 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 = ; + } + + return
+ { dateSep } + +
; + }); + + return
+
{ header }
+
{ evTiles }
+
; } } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 31c1df7b44..7da4dd8644 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -61,10 +61,6 @@ module.exports = React.createClass({ tileShape: PropTypes.string, }, - contextTypes: { - addRichQuote: PropTypes.func, - }, - getInitialState: function() { return { // the URLs (if any) to be previewed with a LinkPreviewWidget @@ -205,21 +201,6 @@ module.exports = React.createClass({ // update the current node with one that's now taken its place node = pillContainer; - } else if (SettingsStore.isFeatureEnabled("feature_rich_quoting") && Quote.isMessageUrl(href)) { - if (this.context.addRichQuote) { // We're already a Rich Quote so just append the next one above - this.context.addRichQuote(href); - node.remove(); - } else { // We're the first in the chain - const quoteContainer = document.createElement('span'); - - const quote = - ; - - ReactDOM.render(quote, quoteContainer); - node.parentNode.replaceChild(quoteContainer, node); - node = quoteContainer; - } - pillified = true; } } else if (node.nodeType == Node.TEXT_NODE) { const Pill = sdk.getComponent('elements.Pill'); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 1d8df4c7a6..7e9a7a75f1 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -18,6 +18,8 @@ limitations under the License. 'use strict'; +import Quote from "../elements/Quote"; + const React = require('react'); import PropTypes from 'prop-types'; const classNames = require("classnames"); @@ -517,7 +519,7 @@ module.exports = withMatrixClient(React.createClass({ if (needsSenderProfile) { let text = null; - if (!this.props.tileShape || this.props.tileShape === 'quote') { + if (!this.props.tileShape) { if (msgtype === 'm.image') text = _td('%(senderName)s sent an image'); else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video'); else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file'); @@ -598,7 +600,6 @@ module.exports = withMatrixClient(React.createClass({ { this._renderE2EPadlock() } { this._renderE2EPadlock() } + { Quote.getQuote(this.props.mxEvent, this.props.onWidgetLoad) } block.getType() !== 'unstyled')) { shouldSendHTML = true; @@ -820,15 +815,15 @@ export default class MessageComposerInput extends React.Component { const md = new Markdown(pt); // if contains no HTML and we're not quoting (needing HTML) - if (md.isPlainText() && !quotingEv) { + if (md.isPlainText()) { contentText = md.toPlaintext(); } else { contentHTML = md.toHTML(); } } - let sendHtmlFn = this.client.sendHtmlMessage; - let sendTextFn = this.client.sendTextMessage; + let sendHtmlFn = ContentHelpers.makeHtmlMessage; + let sendTextFn = ContentHelpers.makeTextMessage; this.historyManager.save( contentState, @@ -839,35 +834,26 @@ export default class MessageComposerInput extends React.Component { contentText = contentText.substring(4); // bit of a hack, but the alternative would be quite complicated if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, ''); - sendHtmlFn = this.client.sendHtmlEmote; - sendTextFn = this.client.sendEmoteMessage; + sendHtmlFn = ContentHelpers.makeHtmlEmote; + sendTextFn = ContentHelpers.makeEmoteMessage; } - if (quotingEv) { - const cli = MatrixClientPeg.get(); - const room = cli.getRoom(quotingEv.getRoomId()); - const sender = room.currentState.getMember(quotingEv.getSender()); - - const {body/*, formatted_body*/} = quotingEv.getContent(); - - const perma = makeEventPermalink(quotingEv.getRoomId(), quotingEv.getId()); - contentText = `${sender.name}:\n> ${body}\n\n${contentText}`; - contentHTML = `Quote
${contentHTML}`; - - // we have finished quoting, clear the quotingEvent - dis.dispatch({ - action: 'quote_event', - event: null, - }); - } + const quotingEv = RoomViewStore.getQuotingEvent(); + const content = quotingEv ? Quote.getRelationship(quotingEv) : {}; + // we have finished quoting, clear the quotingEvent + // TODO maybe delay this until the event actually sends? + dis.dispatch({ + action: 'quote_event', + event: null, + }); let sendMessagePromise; if (contentHTML) { - sendMessagePromise = sendHtmlFn.call( - this.client, this.props.room.roomId, contentText, contentHTML, - ); + Object.assign(content, sendHtmlFn(contentText, contentHTML)); + sendMessagePromise = this.client.sendMessage(this.props.room.roomId, content); } else { - sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText); + Object.assign(content, sendTextFn(contentText)); + sendMessagePromise = this.client.sendMessage(this.props.room.roomId, content); } sendMessagePromise.done((res) => { diff --git a/src/components/views/rooms/QuotePreview.js b/src/components/views/rooms/QuotePreview.js index 614d51dada..590ee21b8f 100644 --- a/src/components/views/rooms/QuotePreview.js +++ b/src/components/views/rooms/QuotePreview.js @@ -71,8 +71,10 @@ export default class QuotePreview extends React.Component { onClick={cancelQuoting} />
- +
; } } + +function dummyOnWidgetLoad() {}