Implement reply chain fallback for threads backwards compatibility (#7565)

This commit is contained in:
Germain 2022-01-19 09:06:48 +00:00 committed by GitHub
parent a00d359422
commit 41b9e4aa4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 40 deletions

View file

@ -55,6 +55,7 @@ interface IProps {
} }
interface IState { interface IState {
thread?: Thread; thread?: Thread;
lastThreadReply?: MatrixEvent;
layout: Layout; layout: Layout;
editState?: EditorStateTransfer; editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
@ -142,14 +143,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
if (!thread) { if (!thread) {
thread = this.props.room.createThread([mxEv]); thread = this.props.room.createThread([mxEv]);
} }
thread.on(ThreadEvent.Update, this.updateThread); thread.on(ThreadEvent.Update, this.updateLastThreadReply);
thread.once(ThreadEvent.Ready, this.updateThread); thread.once(ThreadEvent.Ready, this.updateThread);
this.updateThread(thread); this.updateThread(thread);
}; };
private teardownThread = () => { private teardownThread = () => {
if (this.state.thread) { if (this.state.thread) {
this.state.thread.removeListener(ThreadEvent.Update, this.updateThread); this.state.thread.removeListener(ThreadEvent.Update, this.updateLastThreadReply);
this.state.thread.removeListener(ThreadEvent.Ready, this.updateThread); this.state.thread.removeListener(ThreadEvent.Ready, this.updateThread);
} }
}; };
@ -165,6 +166,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
if (thread && this.state.thread !== thread) { if (thread && this.state.thread !== thread) {
this.setState({ this.setState({
thread, thread,
lastThreadReply: thread.lastReply,
}, () => { }, () => {
thread.emit(ThreadEvent.ViewThread); thread.emit(ThreadEvent.ViewThread);
this.timelinePanelRef.current?.refreshTimeline(); this.timelinePanelRef.current?.refreshTimeline();
@ -172,6 +174,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
} }
}; };
private updateLastThreadReply = () => {
if (this.state.thread) {
this.setState({
lastThreadReply: this.state.thread.lastReply,
});
}
};
private onScroll = (): void => { private onScroll = (): void => {
if (this.props.initialEvent && this.props.isInitialEventHighlighted) { if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
dis.dispatch({ dis.dispatch({
@ -199,8 +209,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
: null; : null;
const threadRelation: IEventRelation = { const threadRelation: IEventRelation = {
rel_type: RelationType.Thread, "rel_type": RelationType.Thread,
event_id: this.state.thread?.id, "event_id": this.state.thread?.id,
"m.in_reply_to": {
"event_id": this.state.lastThreadReply?.getId(),
},
}; };
const messagePanelClassNames = classNames( const messagePanelClassNames = classNames(

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import escapeHtml from "escape-html"; import escapeHtml from "escape-html";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
@ -99,22 +99,8 @@ export default class ReplyChain extends React.Component<IProps, IState> {
public static getParentEventId(ev: MatrixEvent): string | undefined { public static getParentEventId(ev: MatrixEvent): string | undefined {
if (!ev || ev.isRedacted()) return; if (!ev || ev.isRedacted()) return;
if (ev.replyEventId) {
// XXX: For newer relations (annotations, replacements, etc.), we now return ev.replyEventId;
// have a `getRelation` helper on the event, and you might assume it
// could be used here for replies as well... However, the helper
// currently assumes the relation has a `rel_type`, which older replies
// do not, so this block is left as-is for now.
//
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also
// can't just rely on ev.getContent() by itself because historically we
// still show the reply from the original message even though the edit
// event does not include the relation reply.
const mRelatesTo = ev.getContent()['m.relates_to'] || ev.getWireContent()['m.relates_to'];
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
const mInReplyTo = mRelatesTo['m.in_reply_to'];
if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
} else if (!SettingsStore.getValue("feature_thread") && ev.isThreadRelation) { } else if (!SettingsStore.getValue("feature_thread") && ev.isThreadRelation) {
return ev.threadRootId; return ev.threadRootId;
} }
@ -232,7 +218,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
return { body, html }; return { body, html };
} }
public static makeReplyMixIn(ev: MatrixEvent) { public static makeReplyMixIn(ev: MatrixEvent, renderIn?: string[]) {
if (!ev) return {}; if (!ev) return {};
const mixin: any = { const mixin: any = {
@ -243,6 +229,10 @@ export default class ReplyChain extends React.Component<IProps, IState> {
}, },
}; };
if (renderIn) {
mixin['m.relates_to']['m.in_reply_to']['m.render_in'] = renderIn;
}
/** /**
* If the event replied is part of a thread * If the event replied is part of a thread
* Add the `m.thread` relation so that clients * Add the `m.thread` relation so that clients
@ -260,8 +250,21 @@ export default class ReplyChain extends React.Component<IProps, IState> {
return mixin; return mixin;
} }
public static hasReply(event: MatrixEvent) { public static shouldDisplayReply(event: MatrixEvent, renderTarget?: string): boolean {
return Boolean(ReplyChain.getParentEventId(event)); const parentExist = Boolean(ReplyChain.getParentEventId(event));
const relations = event.getRelation();
const renderIn = relations?.["m.in_reply_to"]?.["m.render_in"] ?? [];
const shouldRenderInTarget = !renderTarget || (renderIn.includes(renderTarget));
return parentExist && shouldRenderInTarget;
}
public static getRenderInMixin(relation?: IEventRelation): string[] | undefined {
if (relation?.rel_type === RelationType.Thread) {
return [RelationType.Thread];
}
} }
componentDidMount() { componentDidMount() {

View file

@ -382,7 +382,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
toolbarOpts.push(cancelSendingButton); toolbarOpts.push(cancelSendingButton);
} }
if (this.props.isQuoteExpanded !== undefined && ReplyChain.hasReply(this.props.mxEvent)) { if (this.props.isQuoteExpanded !== undefined && ReplyChain.shouldDisplayReply(this.props.mxEvent)) {
const expandClassName = classNames({ const expandClassName = classNames({
'mx_MessageActionBar_maskButton': true, 'mx_MessageActionBar_maskButton': true,
'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded, 'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded,

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import classNames from "classnames"; import classNames from "classnames";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations"; import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -1330,7 +1330,12 @@ export default class EventTile extends React.Component<IProps, IState> {
msgOption = readAvatars; msgOption = readAvatars;
} }
const replyChain = haveTileForEvent(this.props.mxEvent) && ReplyChain.hasReply(this.props.mxEvent) const renderTarget = this.props.tileShape === TileShape.Thread
? RelationType.Thread
: undefined;
const replyChain = haveTileForEvent(this.props.mxEvent)
&& ReplyChain.shouldDisplayReply(this.props.mxEvent, renderTarget)
? <ReplyChain ? <ReplyChain
parentEv={this.props.mxEvent} parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}

View file

@ -57,23 +57,33 @@ import DocumentPosition from "../../../editor/position";
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands"; import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
interface IAddReplyOpts {
permalinkCreator?: RoomPermalinkCreator;
includeLegacyFallback?: boolean;
renderIn?: string[];
}
function addReplyToMessageContent( function addReplyToMessageContent(
content: IContent, content: IContent,
replyToEvent: MatrixEvent, replyToEvent: MatrixEvent,
permalinkCreator: RoomPermalinkCreator, opts: IAddReplyOpts = {
includeLegacyFallback: true,
},
): void { ): void {
const replyContent = ReplyChain.makeReplyMixIn(replyToEvent); const replyContent = ReplyChain.makeReplyMixIn(replyToEvent, opts.renderIn);
Object.assign(content, replyContent); Object.assign(content, replyContent);
if (opts.includeLegacyFallback) {
// Part of Replies fallback support - prepend the text we're sending // Part of Replies fallback support - prepend the text we're sending
// with the text we're replying to // with the text we're replying to
const nestedReply = ReplyChain.getNestedReplyText(replyToEvent, permalinkCreator); const nestedReply = ReplyChain.getNestedReplyText(replyToEvent, opts.permalinkCreator);
if (nestedReply) { if (nestedReply) {
if (content.formatted_body) { if (content.formatted_body) {
content.formatted_body = nestedReply.html + content.formatted_body; content.formatted_body = nestedReply.html + content.formatted_body;
} }
content.body = nestedReply.body + content.body; content.body = nestedReply.body + content.body;
} }
}
} }
export function attachRelation( export function attachRelation(
@ -94,6 +104,7 @@ export function createMessageContent(
replyToEvent: MatrixEvent, replyToEvent: MatrixEvent,
relation: IEventRelation, relation: IEventRelation,
permalinkCreator: RoomPermalinkCreator, permalinkCreator: RoomPermalinkCreator,
includeReplyLegacyFallback = true,
): IContent { ): IContent {
const isEmote = containsEmote(model); const isEmote = containsEmote(model);
if (isEmote) { if (isEmote) {
@ -116,7 +127,11 @@ export function createMessageContent(
} }
if (replyToEvent) { if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, permalinkCreator); addReplyToMessageContent(content, replyToEvent, {
permalinkCreator,
includeLegacyFallback: true,
renderIn: ReplyChain.getRenderInMixin(relation),
});
} }
if (relation) { if (relation) {
@ -155,6 +170,7 @@ interface ISendMessageComposerProps extends MatrixClientProps {
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
disabled?: boolean; disabled?: boolean;
onChange?(model: EditorModel): void; onChange?(model: EditorModel): void;
includeReplyLegacyFallback?: boolean;
} }
@replaceableComponent("views.rooms.SendMessageComposer") @replaceableComponent("views.rooms.SendMessageComposer")
@ -169,6 +185,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
private dispatcherRef: string; private dispatcherRef: string;
private sendHistoryManager: SendHistoryManager; private sendHistoryManager: SendHistoryManager;
static defaultProps = {
includeReplyLegacyFallback: true,
};
constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) { constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
super(props); super(props);
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) { if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
@ -350,10 +370,14 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
return; // errored return; // errored
} }
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
}
attachRelation(content, this.props.relation); attachRelation(content, this.props.relation);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, {
permalinkCreator: this.props.permalinkCreator,
includeLegacyFallback: true,
renderIn: ReplyChain.getRenderInMixin(this.props.relation),
});
}
} else { } else {
runSlashCommand(cmd, args, this.props.room.roomId, threadId); runSlashCommand(cmd, args, this.props.room.roomId, threadId);
shouldSend = false; shouldSend = false;
@ -378,6 +402,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
replyToEvent, replyToEvent,
this.props.relation, this.props.relation,
this.props.permalinkCreator, this.props.permalinkCreator,
this.props.includeReplyLegacyFallback,
); );
} }
// don't bother sending an empty message // don't bother sending an empty message

View file

@ -302,6 +302,7 @@ describe('<SendMessageComposer/>', () => {
rel_type: RelationType.Thread, rel_type: RelationType.Thread,
event_id: "myFakeThreadId", event_id: "myFakeThreadId",
}} }}
includeReplyLegacyFallback={false}
/> />
</RoomContext.Provider> </RoomContext.Provider>
</MatrixClientContext.Provider>); </MatrixClientContext.Provider>);