Merge remote-tracking branch 'origin/develop' into develop

pull/21833/head
Weblate 2018-01-24 17:53:35 +00:00
commit 6b62acd001
5 changed files with 118 additions and 64 deletions

View File

@ -20,10 +20,11 @@ import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import {wantsDateSeparator} from '../../../DateUtils'; import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk'; 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 // For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) // 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 { export default class Quote extends React.Component {
static isMessageUrl(url) { static isMessageUrl(url) {
@ -32,111 +33,155 @@ export default class Quote extends React.Component {
static childContextTypes = { static childContextTypes = {
matrixClient: PropTypes.object, matrixClient: PropTypes.object,
addRichQuote: PropTypes.func,
}; };
static propTypes = { static propTypes = {
// The matrix.to url of the event // The matrix.to url of the event
url: PropTypes.string, url: PropTypes.string,
// The original node that was rendered
node: PropTypes.instanceOf(Element),
// The parent event // The parent event
parentEv: PropTypes.instanceOf(MatrixEvent), parentEv: PropTypes.instanceOf(MatrixEvent),
// Whether this isn't the first Quote, and we're being nested
isNested: PropTypes.bool,
}; };
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
// The event related to this quote // The event related to this quote and their nested rich quotes
event: null, events: [],
show: !this.props.isNested, // 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.onQuoteClick = this.onQuoteClick.bind(this);
this.addRichQuote = this.addRichQuote.bind(this);
} }
getChildContext() { getChildContext() {
return { return {
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
addRichQuote: this.addRichQuote,
}; };
} }
componentWillReceiveProps(nextProps) { parseUrl(url) {
let roomId; if (!url) return;
let prefix;
let eventId;
if (nextProps.url) {
// Default to the empty array if no match for simplicity // Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing // resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(nextProps.url) || []; const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
roomId = matrixToMatch[1]; // The room ID const [, roomIdentifier, eventId] = matrixToMatch;
prefix = matrixToMatch[2]; // The first character of prefix return {roomIdentifier, eventId};
eventId = matrixToMatch[3]; // The event ID
} }
const room = prefix === '#' ? componentWillReceiveProps(nextProps) {
MatrixClientPeg.get().getRooms().find((r) => { const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
return r.getAliases().includes(roomId); if (!roomIdentifier || !eventId) return;
}) : MatrixClientPeg.get().getRoom(roomId);
const room = this.getRoom(roomIdentifier);
if (!room) return;
// Only try and load the event if we know about the room // 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. // 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() { componentWillMount() {
this.componentWillReceiveProps(this.props); this.componentWillReceiveProps(this.props);
} }
async getEvent(room, eventId) { getRoom(id) {
let event = room.findEventById(eventId); 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) { if (event) {
this.setState({room, event}); this.addEvent(event, show);
return; return;
} }
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId); await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
event = room.findEventById(eventId); this.addEvent(room.findEventById(eventId), show);
this.setState({room, event}); }
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() { onQuoteClick() {
this.setState({ this.setState({ show: true });
show: true,
});
} }
render() { render() {
const ev = this.state.event; const events = this.state.events.slice();
if (ev) { if (events.length) {
if (this.state.show) { 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 EventTile = sdk.getComponent('views.rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
events.forEach((ev) => {
let dateSep = null; let dateSep = null;
const evDate = ev.getDate(); if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
if (wantsDateSeparator(this.props.parentEv.getDate(), evDate)) { dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
const DateSeparator = sdk.getComponent('messages.DateSeparator');
dateSep = <a href={this.props.url}><DateSeparator ts={evDate} /></a>;
} }
return <blockquote className="mx_Quote"> evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
{ dateSep } { dateSep }
<EventTile mxEvent={ev} tileShape="quote" /> <EventTile mxEvent={ev} tileShape="quote" />
</blockquote>; </blockquote>);
} });
return <div> return <div>{ evTiles }</div>;
<a onClick={this.onQuoteClick} className="mx_Quote_show">{ _t('Quote') }</a>
<br />
</div>;
} }
// Deliberately render nothing if the URL isn't recognised // Deliberately render nothing if the URL isn't recognised
return <div> return this.props.node;
<a href={this.props.url}>{ _t('Quote') }</a>
<br />
</div>;
} }
} }

View File

@ -61,6 +61,10 @@ 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
@ -202,19 +206,21 @@ 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)) { } 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. 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 quoteContainer = document.createElement('span');
const quote = const quote =
<Quote url={href} parentEv={this.props.mxEvent} isNested={this.props.tileShape === 'quote'} />; <Quote url={href} parentEv={this.props.mxEvent} node={node} />;
ReactDOM.render(quote, quoteContainer); ReactDOM.render(quote, quoteContainer);
node.parentNode.replaceChild(quoteContainer, node); node.parentNode.replaceChild(quoteContainer, node);
pillified = true;
node = quoteContainer; 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');

View File

@ -592,7 +592,7 @@ module.exports = withMatrixClient(React.createClass({
<div className={classes}> <div className={classes}>
{ avatar } { avatar }
{ sender } { sender }
<div className="mx_EventTile_line"> <div className="mx_EventTile_line mx_EventTile_quote">
<a href={permalink} onClick={this.onPermalinkClicked}> <a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp } { timestamp }
</a> </a>

View File

@ -981,5 +981,6 @@
"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", "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 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",
"<a>In reply to</a> <pill>": "<a>In reply to</a> <pill>",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite." "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite."
} }

View File

@ -132,6 +132,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
quotingEvent: null,
}; };
if (this._state.forwardingEvent) { if (this._state.forwardingEvent) {