initial refactor of Replies to use B explicit over-the-wire format

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2018-02-10 11:19:43 +00:00
parent cf4ae681f4
commit 1c3d8cbe6e
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
5 changed files with 140 additions and 173 deletions

View file

@ -21,33 +21,20 @@ 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"; import {makeUserPermalink} from "../../../matrix-to";
import SettingsStore from "../../../settings/SettingsStore";
// 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\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
export default class Quote extends React.Component { 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 = { 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 // The parent event
parentEv: PropTypes.instanceOf(MatrixEvent), parentEv: PropTypes.instanceOf(MatrixEvent),
onWidgetLoad: PropTypes.func.isRequired,
}; };
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
/*
this.state = { this.state = {
// The event related to this quote and their nested rich quotes // The event related to this quote and their nested rich quotes
events: [], events: [],
@ -55,45 +42,54 @@ export default class Quote extends React.Component {
show: true, show: true,
// Whether an error was encountered fetching nested older event, show node if it does // Whether an error was encountered fetching nested older event, show node if it does
err: false, 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.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() { 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) { 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); const event = room.findEventById(eventId);
if (event) { if (event) return event;
this.addEvent(event, show);
return;
}
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId); await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
this.addEvent(room.findEventById(eventId), show); return room.findEventById(eventId);
}
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({ 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 <Quote parentEv={parentEv} onWidgetLoad={onWidgetLoad} />;
} }
render() { render() {
const events = this.state.events.slice(); let header = null;
if (events.length) { if (this.state.loadedEv) {
const evTiles = []; const ev = this.state.loadedEv;
const Pill = sdk.getComponent('elements.Pill');
if (!this.state.show) { const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
const oldestEv = events.shift(); header = <blockquote className="mx_Quote">
const Pill = sdk.getComponent('elements.Pill'); {
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId()); _t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
evTiles.push(<blockquote className="mx_Quote" key="load"> 'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
{ url={makeUserPermalink(ev.getSender())} shouldShowPillAvatar={true} />,
_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 DateSeparator = sdk.getComponent('messages.DateSeparator');
events.forEach((ev) => {
let dateSep = null;
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
} }
</blockquote>;
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}> } else if (this.state.loading) {
{ dateSep } header = <blockquote>LOADING...</blockquote>;
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>);
});
return <div>{ evTiles }</div>;
} }
// Deliberately render nothing if the URL isn't recognised const EventTile = sdk.getComponent('views.rooms.EventTile');
// in case we get an undefined/falsey node, replace it with null to make React happy const DateSeparator = sdk.getComponent('messages.DateSeparator');
return this.props.node || null; const evTiles = this.state.events.map((ev) => {
let dateSep = null;
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
}
return <blockquote className="mx_Quote" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>;
});
return <div>
<div>{ header }</div>
<div>{ evTiles }</div>
</div>;
} }
} }

View file

@ -61,10 +61,6 @@ 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
@ -205,21 +201,6 @@ 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)) {
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 =
<Quote url={href} parentEv={this.props.mxEvent} node={node} />;
ReactDOM.render(quote, quoteContainer);
node.parentNode.replaceChild(quoteContainer, node);
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

@ -18,6 +18,8 @@ limitations under the License.
'use strict'; 'use strict';
import Quote from "../elements/Quote";
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const classNames = require("classnames"); const classNames = require("classnames");
@ -517,7 +519,7 @@ module.exports = withMatrixClient(React.createClass({
if (needsSenderProfile) { if (needsSenderProfile) {
let text = null; 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'); 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.video') text = _td('%(senderName)s sent a video');
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file'); else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
@ -598,7 +600,6 @@ module.exports = withMatrixClient(React.createClass({
</a> </a>
{ this._renderE2EPadlock() } { this._renderE2EPadlock() }
<EventTileType ref="tile" <EventTileType ref="tile"
tileShape="quote"
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
@ -621,6 +622,7 @@ module.exports = withMatrixClient(React.createClass({
{ timestamp } { timestamp }
</a> </a>
{ this._renderE2EPadlock() } { this._renderE2EPadlock() }
{ Quote.getQuote(this.props.mxEvent, this.props.onWidgetLoad) }
<EventTileType ref="tile" <EventTileType ref="tile"
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
highlights={this.props.highlights} highlights={this.props.highlights}

View file

@ -51,9 +51,11 @@ const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g')
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione'; import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to"; import {makeUserPermalink} from "../../../matrix-to";
import QuotePreview from "./QuotePreview"; import QuotePreview from "./QuotePreview";
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import Quote from "../elements/Quote";
import {ContentHelpers} from 'matrix-js-sdk';
const EMOJI_SHORTNAMES = Object.keys(emojioneList); const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort(); const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
@ -751,17 +753,10 @@ export default class MessageComposerInput extends React.Component {
return true; return true;
} }
const quotingEv = RoomViewStore.getQuotingEvent();
if (this.state.isRichtextEnabled) { if (this.state.isRichtextEnabled) {
// We should only send HTML if any block is styled or contains inline style // We should only send HTML if any block is styled or contains inline style
let shouldSendHTML = false; let shouldSendHTML = false;
// If we are quoting we need HTML Content
if (quotingEv) {
shouldSendHTML = true;
}
const blocks = contentState.getBlocksAsArray(); const blocks = contentState.getBlocksAsArray();
if (blocks.some((block) => block.getType() !== 'unstyled')) { if (blocks.some((block) => block.getType() !== 'unstyled')) {
shouldSendHTML = true; shouldSendHTML = true;
@ -820,15 +815,15 @@ export default class MessageComposerInput extends React.Component {
const md = new Markdown(pt); const md = new Markdown(pt);
// if contains no HTML and we're not quoting (needing HTML) // if contains no HTML and we're not quoting (needing HTML)
if (md.isPlainText() && !quotingEv) { if (md.isPlainText()) {
contentText = md.toPlaintext(); contentText = md.toPlaintext();
} else { } else {
contentHTML = md.toHTML(); contentHTML = md.toHTML();
} }
} }
let sendHtmlFn = this.client.sendHtmlMessage; let sendHtmlFn = ContentHelpers.makeHtmlMessage;
let sendTextFn = this.client.sendTextMessage; let sendTextFn = ContentHelpers.makeTextMessage;
this.historyManager.save( this.historyManager.save(
contentState, contentState,
@ -839,35 +834,26 @@ export default class MessageComposerInput extends React.Component {
contentText = contentText.substring(4); contentText = contentText.substring(4);
// bit of a hack, but the alternative would be quite complicated // bit of a hack, but the alternative would be quite complicated
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, ''); if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
sendHtmlFn = this.client.sendHtmlEmote; sendHtmlFn = ContentHelpers.makeHtmlEmote;
sendTextFn = this.client.sendEmoteMessage; sendTextFn = ContentHelpers.makeEmoteMessage;
} }
if (quotingEv) { const quotingEv = RoomViewStore.getQuotingEvent();
const cli = MatrixClientPeg.get(); const content = quotingEv ? Quote.getRelationship(quotingEv) : {};
const room = cli.getRoom(quotingEv.getRoomId()); // we have finished quoting, clear the quotingEvent
const sender = room.currentState.getMember(quotingEv.getSender()); // TODO maybe delay this until the event actually sends?
dis.dispatch({
const {body/*, formatted_body*/} = quotingEv.getContent(); action: 'quote_event',
event: null,
const perma = makeEventPermalink(quotingEv.getRoomId(), quotingEv.getId()); });
contentText = `${sender.name}:\n> ${body}\n\n${contentText}`;
contentHTML = `<a href="${perma}">Quote<br></a>${contentHTML}`;
// we have finished quoting, clear the quotingEvent
dis.dispatch({
action: 'quote_event',
event: null,
});
}
let sendMessagePromise; let sendMessagePromise;
if (contentHTML) { if (contentHTML) {
sendMessagePromise = sendHtmlFn.call( Object.assign(content, sendHtmlFn(contentText, contentHTML));
this.client, this.props.room.roomId, contentText, contentHTML, sendMessagePromise = this.client.sendMessage(this.props.room.roomId, content);
);
} else { } 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) => { sendMessagePromise.done((res) => {

View file

@ -71,8 +71,10 @@ export default class QuotePreview extends React.Component {
onClick={cancelQuoting} /> onClick={cancelQuoting} />
</div> </div>
<div className="mx_QuotePreview_clear" /> <div className="mx_QuotePreview_clear" />
<EventTile mxEvent={this.state.event} last={true} tileShape="quote" /> <EventTile mxEvent={this.state.event} last={true} tileShape="quote" onWidgetLoad={dummyOnWidgetLoad} />
</div> </div>
</div>; </div>;
} }
} }
function dummyOnWidgetLoad() {}