diff --git a/src/components/views/elements/Quote.js b/src/components/views/elements/Quote.js index bceba0f536..55b59a5789 100644 --- a/src/components/views/elements/Quote.js +++ b/src/components/views/elements/Quote.js @@ -20,10 +20,11 @@ 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\/(([\#\!])[^\/]*)\/(\$[^\/]*)$/; +const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/; export default class Quote extends React.Component { static isMessageUrl(url) { @@ -32,111 +33,148 @@ export default class Quote extends React.Component { 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), - // Whether this isn't the first Quote, and we're being nested - isNested: PropTypes.bool, }; constructor(props, context) { super(props, context); this.state = { - // The event related to this quote - event: null, - show: !this.props.isNested, + // 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 another older event, show if it does + 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) { - let roomId; - let prefix; - let eventId; + const {roomIdentifier, eventId} = this.parseUrl(nextProps.url); + if (!roomIdentifier || !eventId) return; - 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); + 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. - if (room) this.getEvent(room, eventId); + this.setState({ events: [] }); + if (room) this.getEvent(room, eventId, true); } componentWillMount() { this.componentWillReceiveProps(this.props); } - async getEvent(room, eventId) { - let event = room.findEventById(eventId); + 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.setState({room, event}); + this.addEvent(event, show); return; } await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId); - event = room.findEventById(eventId); - this.setState({room, event}); + 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) return; + + const room = this.getRoom(roomIdentifier); + if (!room) return; + + this.getEvent(room, eventId, false); } onQuoteClick() { - this.setState({ - show: true, - }); + this.setState({ show: true }); } render() { - const ev = this.state.event; - if (ev) { - if (this.state.show) { - const EventTile = sdk.getComponent('views.rooms.EventTile'); - let dateSep = null; + const events = this.state.events.slice(); + if (events.length) { + const evTiles = []; - const evDate = ev.getDate(); - if (wantsDateSeparator(this.props.parentEv.getDate(), evDate)) { - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - dateSep = ; - } + if (!this.state.show) { + const oldestEv = events.shift(); + const Pill = sdk.getComponent('elements.Pill'); + const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId()); - return
- { dateSep } - -
; + evTiles.push(
+ { + _t('In reply to ', {}, { + 'a': (sub) => { sub }, + 'pill': , + }) + } +
); } - return
- { _t('Quote') } -
-
; + 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 = ; + } + + evTiles.push(
+ { dateSep } + +
); + }); + + return
{ evTiles }
; } // Deliberately render nothing if the URL isn't recognised - return
- { _t('Quote') } -
-
; + return this.props.node; } } diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 4f34a635dc..31c1df7b44 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -61,6 +61,10 @@ module.exports = React.createClass({ tileShape: PropTypes.string, }, + contextTypes: { + addRichQuote: PropTypes.func, + }, + getInitialState: function() { return { // the URLs (if any) to be previewed with a LinkPreviewWidget @@ -202,18 +206,20 @@ 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)) { - // only allow this branch if we're not already in a quote, as fun as infinite nesting is. - const quoteContainer = document.createElement('span'); + 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); + const quote = + ; + ReactDOM.render(quote, quoteContainer); + node.parentNode.replaceChild(quoteContainer, node); + node = quoteContainer; + } pillified = true; - - node = quoteContainer; } } else if (node.nodeType == Node.TEXT_NODE) { const Pill = sdk.getComponent('elements.Pill'); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 73e3f3a014..52cb8f0991 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -979,5 +979,6 @@ "Which officially provided instance you are using, if any": "Which officially provided instance you are using, if any", "Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor", "Your homeserver's URL": "Your homeserver's URL", - "Your identity server's URL": "Your identity server's URL" + "Your identity server's URL": "Your identity server's URL", + "In reply to ": "In reply to " }