mirror of
https://github.com/element-hq/element-web.git
synced 2024-12-15 23:31:30 +03:00
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:
parent
cf4ae681f4
commit
1c3d8cbe6e
5 changed files with 140 additions and 173 deletions
|
@ -21,33 +21,20 @@ 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\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
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 = {
|
||||
// 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),
|
||||
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
/*
|
||||
this.state = {
|
||||
// The event related to this quote and their nested rich quotes
|
||||
events: [],
|
||||
|
@ -55,45 +42,54 @@ export default class Quote extends React.Component {
|
|||
show: true,
|
||||
// Whether an error was encountered fetching nested older event, show node if it does
|
||||
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.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() {
|
||||
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) {
|
||||
|
@ -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);
|
||||
if (event) {
|
||||
this.addEvent(event, show);
|
||||
return;
|
||||
}
|
||||
if (event) return event;
|
||||
|
||||
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
|
||||
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) {
|
||||
this.setState({ err: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const room = this.getRoom(roomIdentifier);
|
||||
if (!room) {
|
||||
this.setState({ err: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.getEvent(room, eventId, false);
|
||||
return room.findEventById(eventId);
|
||||
}
|
||||
|
||||
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() {
|
||||
const events = this.state.events.slice();
|
||||
if (events.length) {
|
||||
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 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>;
|
||||
let header = null;
|
||||
if (this.state.loadedEv) {
|
||||
const ev = this.state.loadedEv;
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
header = <blockquote className="mx_Quote">
|
||||
{
|
||||
_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(ev.getSender())} shouldShowPillAvatar={true} />,
|
||||
})
|
||||
}
|
||||
|
||||
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
|
||||
{ dateSep }
|
||||
<EventTile mxEvent={ev} tileShape="quote" />
|
||||
</blockquote>);
|
||||
});
|
||||
|
||||
return <div>{ evTiles }</div>;
|
||||
</blockquote>;
|
||||
} else if (this.state.loading) {
|
||||
header = <blockquote>LOADING...</blockquote>;
|
||||
}
|
||||
|
||||
// Deliberately render nothing if the URL isn't recognised
|
||||
// in case we get an undefined/falsey node, replace it with null to make React happy
|
||||
return this.props.node || null;
|
||||
const EventTile = sdk.getComponent('views.rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,10 +61,6 @@ module.exports = React.createClass({
|
|||
tileShape: PropTypes.string,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
addRichQuote: PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
// 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
|
||||
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) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
|
||||
import Quote from "../elements/Quote";
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const classNames = require("classnames");
|
||||
|
@ -517,7 +519,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
if (needsSenderProfile) {
|
||||
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');
|
||||
else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
|
||||
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
|
||||
|
@ -598,7 +600,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
<EventTileType ref="tile"
|
||||
tileShape="quote"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
|
@ -621,6 +622,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{ Quote.getQuote(this.props.mxEvent, this.props.onWidgetLoad) }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
|
|
|
@ -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 SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {makeEventPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
import QuotePreview from "./QuotePreview";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import Quote from "../elements/Quote";
|
||||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
|
||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||
|
@ -751,17 +753,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
const quotingEv = RoomViewStore.getQuotingEvent();
|
||||
|
||||
if (this.state.isRichtextEnabled) {
|
||||
// We should only send HTML if any block is styled or contains inline style
|
||||
let shouldSendHTML = false;
|
||||
|
||||
// If we are quoting we need HTML Content
|
||||
if (quotingEv) {
|
||||
shouldSendHTML = true;
|
||||
}
|
||||
|
||||
const blocks = contentState.getBlocksAsArray();
|
||||
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
||||
shouldSendHTML = true;
|
||||
|
@ -820,15 +815,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
const md = new Markdown(pt);
|
||||
// if contains no HTML and we're not quoting (needing HTML)
|
||||
if (md.isPlainText() && !quotingEv) {
|
||||
if (md.isPlainText()) {
|
||||
contentText = md.toPlaintext();
|
||||
} else {
|
||||
contentHTML = md.toHTML();
|
||||
}
|
||||
}
|
||||
|
||||
let sendHtmlFn = this.client.sendHtmlMessage;
|
||||
let sendTextFn = this.client.sendTextMessage;
|
||||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||
|
||||
this.historyManager.save(
|
||||
contentState,
|
||||
|
@ -839,35 +834,26 @@ export default class MessageComposerInput extends React.Component {
|
|||
contentText = contentText.substring(4);
|
||||
// bit of a hack, but the alternative would be quite complicated
|
||||
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
|
||||
sendHtmlFn = this.client.sendHtmlEmote;
|
||||
sendTextFn = this.client.sendEmoteMessage;
|
||||
sendHtmlFn = ContentHelpers.makeHtmlEmote;
|
||||
sendTextFn = ContentHelpers.makeEmoteMessage;
|
||||
}
|
||||
|
||||
if (quotingEv) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(quotingEv.getRoomId());
|
||||
const sender = room.currentState.getMember(quotingEv.getSender());
|
||||
|
||||
const {body/*, formatted_body*/} = quotingEv.getContent();
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
const quotingEv = RoomViewStore.getQuotingEvent();
|
||||
const content = quotingEv ? Quote.getRelationship(quotingEv) : {};
|
||||
// we have finished quoting, clear the quotingEvent
|
||||
// TODO maybe delay this until the event actually sends?
|
||||
dis.dispatch({
|
||||
action: 'quote_event',
|
||||
event: null,
|
||||
});
|
||||
|
||||
let sendMessagePromise;
|
||||
if (contentHTML) {
|
||||
sendMessagePromise = sendHtmlFn.call(
|
||||
this.client, this.props.room.roomId, contentText, contentHTML,
|
||||
);
|
||||
Object.assign(content, sendHtmlFn(contentText, contentHTML));
|
||||
sendMessagePromise = this.client.sendMessage(this.props.room.roomId, content);
|
||||
} 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) => {
|
||||
|
|
|
@ -71,8 +71,10 @@ export default class QuotePreview extends React.Component {
|
|||
onClick={cancelQuoting} />
|
||||
</div>
|
||||
<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>;
|
||||
}
|
||||
}
|
||||
|
||||
function dummyOnWidgetLoad() {}
|
||||
|
|
Loading…
Reference in a new issue