From 37dcb33b15e9f3848f6badbdb4125c7dbf105c82 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Jan 2018 14:45:04 +0000 Subject: [PATCH 1/5] Reset quotingEvent on Room Change because inideal UX Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/stores/RoomViewStore.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index cf895d2190..4c010f4e8e 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -132,6 +132,8 @@ class RoomViewStore extends Store { shouldPeek: payload.should_peek === undefined ? true : payload.should_peek, // have we sent a join request for this room and are waiting for a response? joining: payload.joining || false, + // Reset quotingEvent because we don't want cross-room because bad UX + quotingEvent: null, }; if (this._state.forwardingEvent) { From 33995b053eb00baab5e8e80c37000326dc2e2cc8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Jan 2018 16:34:14 +0000 Subject: [PATCH 2/5] Add class to EventTile Line's that are quotes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/EventTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 937826b4fd..4e0ff2c6d6 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -592,7 +592,7 @@ module.exports = withMatrixClient(React.createClass({
{ avatar } { sender } -
+
{ timestamp } From 3b1d69edbb01ff534ca53c8a58a3c79a08816cec Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Jan 2018 16:34:47 +0000 Subject: [PATCH 3/5] Change Quotes to be linearly rendered rather than recursively nested Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Quote.js | 146 ++++++++++++------- src/components/views/messages/TextualBody.js | 24 +-- src/i18n/strings/en_EN.json | 3 +- 3 files changed, 109 insertions(+), 64 deletions(-) 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 " } From a6cefb83f86f4fe0050e1af18e81d5c631b97024 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Jan 2018 16:41:32 +0000 Subject: [PATCH 4/5] basic error handling for malformed quotes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Quote.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/Quote.js b/src/components/views/elements/Quote.js index 55b59a5789..0a186b3ca6 100644 --- a/src/components/views/elements/Quote.js +++ b/src/components/views/elements/Quote.js @@ -53,8 +53,8 @@ export default class Quote extends React.Component { 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, + // Whether an error was encountered fetching nested older event, show node if it does + err: false, }; this.onQuoteClick = this.onQuoteClick.bind(this); @@ -124,10 +124,16 @@ export default class Quote extends React.Component { // addRichQuote(roomId, eventId) { addRichQuote(href) { const {roomIdentifier, eventId} = this.parseUrl(href); - if (!roomIdentifier || !eventId) return; + if (!roomIdentifier || !eventId) { + this.setState({ err: true }); + return; + } const room = this.getRoom(roomIdentifier); - if (!room) return; + if (!room) { + this.setState({ err: true }); + return; + } this.getEvent(room, eventId, false); } From e3e3535072bb053eb5c41a3c0aca89920b984fd5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Jan 2018 17:02:20 +0000 Subject: [PATCH 5/5] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Quote.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Quote.js b/src/components/views/elements/Quote.js index 0a186b3ca6..0c0078978d 100644 --- a/src/components/views/elements/Quote.js +++ b/src/components/views/elements/Quote.js @@ -156,7 +156,8 @@ export default class Quote extends React.Component { { _t('In reply to ', {}, { 'a': (sub) => { sub }, - 'pill': , + 'pill': , }) } );