Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Implement reply chain fallback for threads backwards compatibility #7565

Merged
merged 6 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/components/structures/ThreadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ interface IProps {
}
interface IState {
thread?: Thread;
lastThreadReply?: MatrixEvent;
layout: Layout;
editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent;
Expand Down Expand Up @@ -142,14 +143,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
if (!thread) {
thread = this.props.room.createThread([mxEv]);
}
thread.on(ThreadEvent.Update, this.updateThread);
thread.on(ThreadEvent.Update, this.updateLastThreadReply);
thread.once(ThreadEvent.Ready, this.updateThread);
this.updateThread(thread);
};

private teardownThread = () => {
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);
}
};
Expand All @@ -165,13 +166,22 @@ export default class ThreadView extends React.Component<IProps, IState> {
if (thread && this.state.thread !== thread) {
this.setState({
thread,
lastThreadReply: thread.lastReply,
}, () => {
thread.emit(ThreadEvent.ViewThread);
this.timelinePanelRef.current?.refreshTimeline();
});
}
};

private updateLastThreadReply = () => {
if (this.state.thread) {
this.setState({
lastThreadReply: this.state.thread.lastReply,
});
}
};

private onScroll = (): void => {
if (this.props.initialEvent && this.props.isInitialEventHighlighted) {
dis.dispatch({
Expand Down Expand Up @@ -199,8 +209,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
: null;

const threadRelation: IEventRelation = {
rel_type: RelationType.Thread,
event_id: this.state.thread?.id,
"rel_type": RelationType.Thread,
"event_id": this.state.thread?.id,
"m.in_reply_to": {
"event_id": this.state.lastThreadReply?.getId(),
"m.render_in": ["m.room"],
},
};

const messagePanelClassNames = classNames(
Expand Down
35 changes: 27 additions & 8 deletions src/components/views/elements/ReplyChain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.

import React from 'react';
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 sanitizeHtml from "sanitize-html";
import { Room } from 'matrix-js-sdk/src/models/room';
Expand Down Expand Up @@ -111,10 +111,8 @@ export default class ReplyChain extends React.Component<IProps, IState> {
// 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.
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
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'];
if (ev.replyEventId) {
return ev.replyEventId;
} else if (!SettingsStore.getValue("feature_thread") && ev.isThreadRelation) {
return ev.threadRootId;
}
Expand Down Expand Up @@ -232,7 +230,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
return { body, html };
}

public static makeReplyMixIn(ev: MatrixEvent) {
public static makeReplyMixIn(ev: MatrixEvent, renderIn?: string[]) {
if (!ev) return {};

const mixin: any = {
Expand All @@ -243,6 +241,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
* Add the `m.thread` relation so that clients
Expand All @@ -260,8 +262,25 @@ export default class ReplyChain extends React.Component<IProps, IState> {
return mixin;
}

public static hasReply(event: MatrixEvent) {
return Boolean(ReplyChain.getParentEventId(event));
private static DEFAULT_RENDER_TARGET = "m.room";

public static shouldDisplayReply(event: MatrixEvent, renderTarget = ReplyChain.DEFAULT_RENDER_TARGET): boolean {
const parentExist = Boolean(ReplyChain.getParentEventId(event));

const relations = event.getWireContent()["m.relates_to"];
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
const renderIn = relations?.["m.in_reply_to"]?.["m.render_in"];

const shouldRenderInTarget = !renderIn || renderIn.includes(renderTarget);

return parentExist && shouldRenderInTarget;
}

public static getRenderInMixin(relation?: IEventRelation): string[] {
const renderIn = ["m.room"];
if (relation?.rel_type === RelationType.Thread) {
renderIn.push(RelationType.Thread);
}
return renderIn;
}

componentDidMount() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/MessageActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
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({
'mx_MessageActionBar_maskButton': true,
'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded,
Expand Down
9 changes: 7 additions & 2 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.

import React, { createRef } from 'react';
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 { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
Expand Down Expand Up @@ -1330,7 +1330,12 @@ export default class EventTile extends React.Component<IProps, IState> {
msgOption = readAvatars;
}

const replyChain = haveTileForEvent(this.props.mxEvent) && ReplyChain.hasReply(this.props.mxEvent)
const renderTarget = this.props.tileShape === TileShape.Thread
? RelationType.Thread
: "m.room";

const replyChain = haveTileForEvent(this.props.mxEvent)
&& ReplyChain.shouldDisplayReply(this.props.mxEvent, renderTarget)
? <ReplyChain
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
Expand Down
49 changes: 37 additions & 12 deletions src/components/views/rooms/SendMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,32 @@ import DocumentPosition from "../../../editor/position";
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";

interface IAddReplyOpts {
permalinkCreator?: RoomPermalinkCreator;
includeLegacyFallback?: boolean;
renderIn?: string[];
}

function addReplyToMessageContent(
content: IContent,
replyToEvent: MatrixEvent,
permalinkCreator: RoomPermalinkCreator,
opts: IAddReplyOpts = {
includeLegacyFallback: true,
},
): void {
const replyContent = ReplyChain.makeReplyMixIn(replyToEvent);
const replyContent = ReplyChain.makeReplyMixIn(replyToEvent, opts.renderIn);
Object.assign(content, replyContent);

// Part of Replies fallback support - prepend the text we're sending
// with the text we're replying to
const nestedReply = ReplyChain.getNestedReplyText(replyToEvent, permalinkCreator);
if (nestedReply) {
if (content.formatted_body) {
content.formatted_body = nestedReply.html + content.formatted_body;
if (opts.includeLegacyFallback) {
// Part of Replies fallback support - prepend the text we're sending
// with the text we're replying to
const nestedReply = ReplyChain.getNestedReplyText(replyToEvent, opts.permalinkCreator);
if (nestedReply) {
if (content.formatted_body) {
content.formatted_body = nestedReply.html + content.formatted_body;
}
content.body = nestedReply.body + content.body;
}
content.body = nestedReply.body + content.body;
}
}

Expand All @@ -94,6 +104,7 @@ export function createMessageContent(
replyToEvent: MatrixEvent,
relation: IEventRelation,
permalinkCreator: RoomPermalinkCreator,
includeReplyLegacyFallback = true,
): IContent {
const isEmote = containsEmote(model);
if (isEmote) {
Expand All @@ -116,7 +127,11 @@ export function createMessageContent(
}

if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, permalinkCreator);
addReplyToMessageContent(content, replyToEvent, {
permalinkCreator,
includeLegacyFallback: true,
renderIn: ReplyChain.getRenderInMixin(relation),
});
}

if (relation) {
Expand Down Expand Up @@ -155,6 +170,7 @@ interface ISendMessageComposerProps extends MatrixClientProps {
replyToEvent?: MatrixEvent;
disabled?: boolean;
onChange?(model: EditorModel): void;
includeReplyLegacyFallback: boolean;
}

@replaceableComponent("views.rooms.SendMessageComposer")
Expand All @@ -169,6 +185,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
private dispatcherRef: string;
private sendHistoryManager: SendHistoryManager;

static defaultProps = {
includeReplyLegacyFallback: true,
};

constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
super(props);
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
Expand Down Expand Up @@ -350,10 +370,14 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
return; // errored
}

attachRelation(content, this.props.relation);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
addReplyToMessageContent(content, replyToEvent, {
permalinkCreator: this.props.permalinkCreator,
includeLegacyFallback: true,
renderIn: ReplyChain.getRenderInMixin(this.props.relation),
});
}
attachRelation(content, this.props.relation);
} else {
runSlashCommand(cmd, args, this.props.room.roomId, threadId);
shouldSend = false;
Expand All @@ -378,6 +402,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
replyToEvent,
this.props.relation,
this.props.permalinkCreator,
this.props.includeReplyLegacyFallback,
);
}
// don't bother sending an empty message
Expand Down
1 change: 1 addition & 0 deletions test/components/views/rooms/SendMessageComposer-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ describe('<SendMessageComposer/>', () => {
rel_type: RelationType.Thread,
event_id: "myFakeThreadId",
}}
includeReplyLegacyFallback={false}
/>
</RoomContext.Provider>
</MatrixClientContext.Provider>);
Expand Down