Implement reply chain fallback for threads backwards compatibility (#7565)
This commit is contained in:
parent
a00d359422
commit
41b9e4aa4f
6 changed files with 87 additions and 40 deletions
|
@ -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(
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -57,17 +57,26 @@ 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;
|
||||||
|
@ -75,6 +84,7 @@ function addReplyToMessageContent(
|
||||||
content.body = nestedReply.body + content.body;
|
content.body = nestedReply.body + content.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function attachRelation(
|
export function attachRelation(
|
||||||
content: IContent,
|
content: IContent,
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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>);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue