2017-12-10 15:50:41 +03:00
|
|
|
/*
|
|
|
|
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';
|
2017-12-15 21:39:01 +03:00
|
|
|
import {_t} from '../../../languageHandler';
|
2017-12-10 15:50:41 +03:00
|
|
|
import PropTypes from 'prop-types';
|
2018-02-17 00:17:41 +03:00
|
|
|
import dis from '../../../dispatcher';
|
2017-12-10 15:50:41 +03:00
|
|
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
2018-01-10 15:06:24 +03:00
|
|
|
import {wantsDateSeparator} from '../../../DateUtils';
|
2017-12-15 21:39:01 +03:00
|
|
|
import {MatrixEvent} from 'matrix-js-sdk';
|
2018-03-04 15:39:34 +03:00
|
|
|
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
|
2018-02-10 14:19:43 +03:00
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-02-19 17:27:10 +03:00
|
|
|
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
|
|
|
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
|
|
|
|
// be low as each event being loaded (after the first) is triggered by an explicit user action.
|
2018-03-04 15:39:34 +03:00
|
|
|
export default class ReplyThread extends React.Component {
|
2017-12-17 23:20:45 +03:00
|
|
|
static propTypes = {
|
2018-02-19 17:27:10 +03:00
|
|
|
// the latest event in this chain of replies
|
2017-12-15 21:39:01 +03:00
|
|
|
parentEv: PropTypes.instanceOf(MatrixEvent),
|
2018-02-19 17:27:10 +03:00
|
|
|
// called when the preview's contents has loaded
|
2018-02-10 14:19:43 +03:00
|
|
|
onWidgetLoad: PropTypes.func.isRequired,
|
2017-12-17 23:20:45 +03:00
|
|
|
};
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2017-12-17 23:20:45 +03:00
|
|
|
constructor(props, context) {
|
|
|
|
super(props, context);
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
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.
|
2018-02-10 18:45:42 +03:00
|
|
|
err: false,
|
2017-12-10 15:50:41 +03:00
|
|
|
};
|
2017-12-18 22:28:01 +03:00
|
|
|
|
2018-01-10 14:51:23 +03:00
|
|
|
this.onQuoteClick = this.onQuoteClick.bind(this);
|
2017-12-17 23:20:45 +03:00
|
|
|
}
|
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
componentWillMount() {
|
2018-02-19 17:28:31 +03:00
|
|
|
this.unmounted = false;
|
2018-02-20 02:42:04 +03:00
|
|
|
this.room = MatrixClientPeg.get().getRoom(this.props.parentEv.getRoomId());
|
2018-02-10 14:19:43 +03:00
|
|
|
this.initialize();
|
2017-12-17 23:20:45 +03:00
|
|
|
}
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-02-10 19:01:19 +03:00
|
|
|
componentWillUnmount() {
|
|
|
|
this.unmounted = true;
|
|
|
|
}
|
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
async initialize() {
|
|
|
|
const {parentEv} = this.props;
|
2018-03-04 15:39:34 +03:00
|
|
|
const inReplyTo = ReplyThread.getInReplyTo(parentEv);
|
2018-02-20 02:42:04 +03:00
|
|
|
if (!inReplyTo) {
|
|
|
|
this.setState({err: true});
|
|
|
|
return;
|
|
|
|
}
|
2018-01-22 19:34:47 +03:00
|
|
|
|
2018-03-04 15:39:34 +03:00
|
|
|
const ev = await ReplyThread.getEvent(this.room, inReplyTo['event_id']);
|
2018-02-10 19:01:19 +03:00
|
|
|
if (this.unmounted) return;
|
|
|
|
|
2018-02-10 18:45:42 +03:00
|
|
|
if (ev) {
|
|
|
|
this.setState({
|
|
|
|
events: [ev],
|
|
|
|
}, this.loadNextEvent);
|
|
|
|
} else {
|
|
|
|
this.setState({err: true});
|
|
|
|
}
|
2018-01-22 19:34:47 +03:00
|
|
|
}
|
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
async loadNextEvent() {
|
2018-02-20 02:42:04 +03:00
|
|
|
if (this.unmounted) return;
|
2018-02-10 14:19:43 +03:00
|
|
|
const ev = this.state.events[0];
|
2018-03-04 15:39:34 +03:00
|
|
|
const inReplyTo = ReplyThread.getInReplyTo(ev);
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
if (!inReplyTo) {
|
|
|
|
this.setState({
|
|
|
|
loading: false,
|
2018-02-17 00:24:42 +03:00
|
|
|
}, this.props.onWidgetLoad);
|
2018-02-10 14:19:43 +03:00
|
|
|
return;
|
|
|
|
}
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-03-04 15:39:34 +03:00
|
|
|
const loadedEv = await ReplyThread.getEvent(this.room, inReplyTo['event_id']);
|
2018-02-10 19:01:19 +03:00
|
|
|
if (this.unmounted) return;
|
|
|
|
|
2018-02-10 18:45:42 +03:00
|
|
|
if (loadedEv) {
|
2018-02-17 00:24:42 +03:00
|
|
|
this.setState({loadedEv}, this.props.onWidgetLoad);
|
2018-02-10 18:45:42 +03:00
|
|
|
} else {
|
2018-02-17 00:24:42 +03:00
|
|
|
this.setState({err: true}, this.props.onWidgetLoad);
|
2018-02-10 18:45:42 +03:00
|
|
|
}
|
2017-12-17 23:20:45 +03:00
|
|
|
}
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-02-20 02:42:04 +03:00
|
|
|
static async getEvent(room, eventId) {
|
2018-01-22 19:34:47 +03:00
|
|
|
const event = room.findEventById(eventId);
|
2018-02-10 14:19:43 +03:00
|
|
|
if (event) return event;
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-02-10 18:45:42 +03:00
|
|
|
try {
|
|
|
|
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
|
|
|
|
} catch (e) {
|
2018-02-20 02:42:04 +03:00
|
|
|
return null;
|
2018-02-10 18:45:42 +03:00
|
|
|
}
|
2018-02-10 14:19:43 +03:00
|
|
|
return room.findEventById(eventId);
|
2018-01-22 19:34:47 +03:00
|
|
|
}
|
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
onQuoteClick() {
|
|
|
|
const events = [this.state.loadedEv].concat(this.state.events);
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
loadedEv: null,
|
|
|
|
events,
|
|
|
|
}, this.loadNextEvent);
|
2018-02-17 00:17:41 +03:00
|
|
|
|
|
|
|
dis.dispatch({action: 'focus_composer'});
|
2018-01-22 19:34:47 +03:00
|
|
|
}
|
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
static getInReplyTo(ev) {
|
2018-03-04 15:39:34 +03:00
|
|
|
if (!ev || ev.isRedacted()) return;
|
2018-01-22 19:34:47 +03:00
|
|
|
|
2018-02-10 15:31:22 +03:00
|
|
|
const mRelatesTo = ev.getWireContent()['m.relates_to'];
|
2018-02-11 14:45:13 +03:00
|
|
|
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
2018-02-10 15:31:22 +03:00
|
|
|
const mInReplyTo = mRelatesTo['m.in_reply_to'];
|
2018-02-11 14:45:13 +03:00
|
|
|
if (mInReplyTo['event_id']) return mInReplyTo;
|
2018-01-22 19:41:32 +03:00
|
|
|
}
|
2018-02-10 14:19:43 +03:00
|
|
|
}
|
2018-01-22 19:34:47 +03:00
|
|
|
|
2018-03-04 15:39:34 +03:00
|
|
|
// Part of Replies fallback support
|
|
|
|
static stripPlainReply(body) {
|
|
|
|
const lines = body.split('\n');
|
|
|
|
while (lines[0].startsWith('> ')) lines.shift();
|
|
|
|
return lines.join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Part of Replies fallback support
|
|
|
|
static stripHTMLReply(html) {
|
|
|
|
return html.replace(/^<blockquote data-mx-reply>[\s\S]+?<\/blockquote>/, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Part of Replies fallback support
|
|
|
|
static getNestedReplyText(ev) {
|
|
|
|
if (!ev) return null;
|
|
|
|
|
|
|
|
let {body, formatted_body: html} = ev.getContent();
|
|
|
|
if (this.getInReplyTo(ev)) {
|
|
|
|
if (body) body = this.stripPlainReply(body);
|
|
|
|
if (html) html = this.stripHTMLReply(html);
|
|
|
|
}
|
|
|
|
|
|
|
|
html = `<blockquote data-mx-reply><a href="${makeEventPermalink(ev.getRoomId(), ev.getId())}">In reply to</a> `
|
|
|
|
+ `<a href="${makeUserPermalink(ev.getSender())}">${ev.getSender()}</a> ${html || body}</blockquote>`;
|
|
|
|
// `<${ev.getSender()}> ${html || body}</blockquote>`;
|
|
|
|
const lines = body.split('\n');
|
|
|
|
const first = `> <${ev.getSender()}> ${lines.shift()}`;
|
|
|
|
body = first + lines.map((line) => `> ${line}`).join('\n') + '\n';
|
|
|
|
|
|
|
|
return {body, html};
|
|
|
|
}
|
|
|
|
|
|
|
|
static getReplyEvContent(ev) {
|
2018-02-20 02:42:04 +03:00
|
|
|
if (!ev) return {};
|
2018-02-10 14:19:43 +03:00
|
|
|
return {
|
|
|
|
'm.relates_to': {
|
|
|
|
'm.in_reply_to': {
|
|
|
|
'event_id': ev.getId(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
2017-12-17 23:20:45 +03:00
|
|
|
}
|
2017-12-10 15:50:41 +03:00
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
static getQuote(parentEv, onWidgetLoad) {
|
2018-03-04 15:39:34 +03:00
|
|
|
if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !ReplyThread.getInReplyTo(parentEv)) {
|
|
|
|
return <div />;
|
|
|
|
}
|
|
|
|
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} />;
|
2017-12-18 22:28:01 +03:00
|
|
|
}
|
|
|
|
|
2017-12-17 23:20:45 +03:00
|
|
|
render() {
|
2018-02-10 14:19:43 +03:00
|
|
|
let header = null;
|
2018-02-10 18:45:42 +03:00
|
|
|
|
|
|
|
if (this.state.err) {
|
2018-02-20 18:40:19 +03:00
|
|
|
header = <blockquote className="mx_ReplyThread mx_ReplyThread_error">
|
2018-02-10 18:45:42 +03:00
|
|
|
{
|
|
|
|
_t('Unable to load event that was replied to, ' +
|
|
|
|
'it either does not exist or you do not have permission to view it.')
|
|
|
|
}
|
|
|
|
</blockquote>;
|
|
|
|
} else if (this.state.loadedEv) {
|
2018-02-10 14:19:43 +03:00
|
|
|
const ev = this.state.loadedEv;
|
|
|
|
const Pill = sdk.getComponent('elements.Pill');
|
|
|
|
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
2018-02-20 18:40:19 +03:00
|
|
|
header = <blockquote className="mx_ReplyThread">
|
2018-02-10 14:19:43 +03:00
|
|
|
{
|
|
|
|
_t('<a>In reply to</a> <pill>', {}, {
|
2018-02-20 18:40:19 +03:00
|
|
|
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
|
2018-02-10 14:19:43 +03:00
|
|
|
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
|
|
|
|
url={makeUserPermalink(ev.getSender())} shouldShowPillAvatar={true} />,
|
|
|
|
})
|
2017-12-18 22:28:01 +03:00
|
|
|
}
|
2018-02-10 14:19:43 +03:00
|
|
|
</blockquote>;
|
|
|
|
} else if (this.state.loading) {
|
2018-02-10 18:45:42 +03:00
|
|
|
const Spinner = sdk.getComponent("elements.Spinner");
|
2018-02-10 18:47:32 +03:00
|
|
|
header = <Spinner w={16} h={16} />;
|
2018-02-10 14:19:43 +03:00
|
|
|
}
|
2017-12-18 22:28:01 +03:00
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
const EventTile = sdk.getComponent('views.rooms.EventTile');
|
|
|
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
|
|
|
const evTiles = this.state.events.map((ev) => {
|
|
|
|
let dateSep = null;
|
2017-12-15 21:39:01 +03:00
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
|
|
|
|
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
|
|
|
|
}
|
|
|
|
|
2018-02-20 18:40:19 +03:00
|
|
|
return <blockquote className="mx_ReplyThread" key={ev.getId()}>
|
2018-02-10 14:19:43 +03:00
|
|
|
{ dateSep }
|
2018-02-17 00:14:03 +03:00
|
|
|
<EventTile mxEvent={ev}
|
|
|
|
tileShape="reply"
|
|
|
|
onWidgetLoad={this.props.onWidgetLoad}
|
|
|
|
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
|
2018-02-10 14:19:43 +03:00
|
|
|
</blockquote>;
|
|
|
|
});
|
2017-12-10 15:54:19 +03:00
|
|
|
|
2018-02-10 14:19:43 +03:00
|
|
|
return <div>
|
|
|
|
<div>{ header }</div>
|
|
|
|
<div>{ evTiles }</div>
|
|
|
|
</div>;
|
2017-12-17 23:20:45 +03:00
|
|
|
}
|
|
|
|
}
|