Skip to content

Commit 1e060fe

Browse files
authored
Don't form continuations from thread roots (matrix-org#8166)
* Don't form continuations from thread roots * Only apply the continuation break in the main timeline
1 parent 6c69f3e commit 1e060fe

File tree

5 files changed

+62
-11
lines changed

5 files changed

+62
-11
lines changed

src/components/structures/MessagePanel.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export function shouldFormContinuation(
6969
prevEvent: MatrixEvent,
7070
mxEvent: MatrixEvent,
7171
showHiddenEvents: boolean,
72+
threadsEnabled: boolean,
7273
timelineRenderingType?: TimelineRenderingType,
7374
): boolean {
7475
if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false;
@@ -90,6 +91,10 @@ export function shouldFormContinuation(
9091
mxEvent.sender.name !== prevEvent.sender.name ||
9192
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
9293

94+
// Thread summaries in the main timeline should break up a continuation
95+
if (threadsEnabled && prevEvent.isThreadRoot &&
96+
timelineRenderingType !== TimelineRenderingType.Thread) return false;
97+
9398
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
9499
if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
95100

@@ -241,6 +246,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
241246
private readReceiptsByUserId: Record<string, IReadReceiptForUser> = {};
242247

243248
private readonly showHiddenEventsInTimeline: boolean;
249+
private readonly threadsEnabled: boolean;
244250
private isMounted = false;
245251

246252
private readMarkerNode = createRef<HTMLLIElement>();
@@ -264,10 +270,11 @@ export default class MessagePanel extends React.Component<IProps, IState> {
264270
hideSender: this.shouldHideSender(),
265271
};
266272

267-
// Cache hidden events setting on mount since Settings is expensive to
268-
// query, and we check this in a hot code path. This is also cached in
269-
// our RoomContext, however we still need a fallback for roomless MessagePanels.
273+
// Cache these settings on mount since Settings is expensive to query,
274+
// and we check this in a hot code path. This is also cached in our
275+
// RoomContext, however we still need a fallback for roomless MessagePanels.
270276
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
277+
this.threadsEnabled = SettingsStore.getValue("feature_thread");
271278

272279
this.showTypingNotificationsWatcherRef =
273280
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
@@ -465,7 +472,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
465472

466473
// TODO: Implement granular (per-room) hide options
467474
public shouldShowEvent(mxEv: MatrixEvent, forceHideEvents = false): boolean {
468-
if (this.props.hideThreadedMessages && SettingsStore.getValue("feature_thread")) {
475+
if (this.props.hideThreadedMessages && this.threadsEnabled) {
469476
if (mxEv.isThreadRelation) {
470477
return false;
471478
}
@@ -744,12 +751,16 @@ export default class MessagePanel extends React.Component<IProps, IState> {
744751
lastInSection = willWantDateSeparator ||
745752
mxEv.getSender() !== nextEv.getSender() ||
746753
getEventDisplayInfo(nextEv).isInfoMessage ||
747-
!shouldFormContinuation(mxEv, nextEv, this.showHiddenEvents, this.context.timelineRenderingType);
754+
!shouldFormContinuation(
755+
mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType,
756+
);
748757
}
749758

750759
// is this a continuation of the previous message?
751760
const continuation = !wantsDateSeparator &&
752-
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents, this.context.timelineRenderingType);
761+
shouldFormContinuation(
762+
prevEvent, mxEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType,
763+
);
753764

754765
const eventId = mxEv.getId();
755766
const highlight = (eventId === this.props.highlightedEventId);

src/components/views/rooms/SearchResultTile.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export default class SearchResultTile extends React.Component<IProps> {
6868
const layout = SettingsStore.getValue("layout");
6969
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
7070
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
71+
const threadsEnabled = SettingsStore.getValue("feature_thread");
7172

7273
const timeline = result.context.getTimeline();
7374
for (let j = 0; j < timeline.length; j++) {
@@ -88,6 +89,7 @@ export default class SearchResultTile extends React.Component<IProps> {
8889
prevEv,
8990
mxEv,
9091
this.context?.showHiddenEventsInTimeline,
92+
threadsEnabled,
9193
TimelineRenderingType.Search,
9294
);
9395

@@ -102,6 +104,7 @@ export default class SearchResultTile extends React.Component<IProps> {
102104
mxEv,
103105
nextEv,
104106
this.context?.showHiddenEventsInTimeline,
107+
threadsEnabled,
105108
TimelineRenderingType.Search,
106109
)
107110
);

src/utils/exportUtils/HtmlExport.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
2323
import { logger } from "matrix-js-sdk/src/logger";
2424

2525
import Exporter from "./Exporter";
26+
import SettingsStore from "../../settings/SettingsStore";
2627
import { mediaFromMxc } from "../../customisations/Media";
2728
import { Layout } from "../../settings/enums/Layout";
2829
import { shouldFormContinuation } from "../../components/structures/MessagePanel";
@@ -46,6 +47,7 @@ export default class HTMLExporter extends Exporter {
4647
protected permalinkCreator: RoomPermalinkCreator;
4748
protected totalSize: number;
4849
protected mediaOmitText: string;
50+
private threadsEnabled: boolean;
4951

5052
constructor(
5153
room: Room,
@@ -60,6 +62,7 @@ export default class HTMLExporter extends Exporter {
6062
this.mediaOmitText = !this.exportOptions.attachmentsIncluded
6163
? _t("Media omitted")
6264
: _t("Media omitted - file size limit exceeded");
65+
this.threadsEnabled = SettingsStore.getValue("feature_thread");
6366
}
6467

6568
protected async getRoomAvatar() {
@@ -406,8 +409,8 @@ export default class HTMLExporter extends Exporter {
406409
if (!haveTileForEvent(event)) continue;
407410

408411
content += this.needsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : "";
409-
const shouldBeJoined = !this.needsDateSeparator(event, prevEvent)
410-
&& shouldFormContinuation(prevEvent, event, false);
412+
const shouldBeJoined = !this.needsDateSeparator(event, prevEvent) &&
413+
shouldFormContinuation(prevEvent, event, false, this.threadsEnabled);
411414
const body = await this.createMessageBody(event, shouldBeJoined);
412415
this.totalSize += Buffer.byteLength(body);
413416
content += body;

test/components/structures/MessagePanel-test.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@ import * as TestUtils from "react-dom/test-utils";
2525

2626
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
2727
import sdk from '../../skinned-sdk';
28+
import MessagePanel, { shouldFormContinuation } from "../../../src/components/structures/MessagePanel";
2829
import SettingsStore from "../../../src/settings/SettingsStore";
2930
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
3031
import RoomContext from "../../../src/contexts/RoomContext";
3132
import DMRoomMap from "../../../src/utils/DMRoomMap";
3233
import { UnwrappedEventTile } from "../../../src/components/views/rooms/EventTile";
3334
import * as TestUtilsMatrix from "../../test-utils";
3435

35-
const MessagePanel = sdk.getComponent('structures.MessagePanel');
36-
3736
let client;
3837
const room = new Matrix.Room("!roomId:server_name");
3938

@@ -594,3 +593,25 @@ describe('MessagePanel', function() {
594593
expect(els.last().prop("events").length).toEqual(5);
595594
});
596595
});
596+
597+
describe("shouldFormContinuation", () => {
598+
it("does not form continuations from thread roots", () => {
599+
const threadRoot = TestUtilsMatrix.mkMessage({
600+
event: true,
601+
room: "!room:id",
602+
user: "@user:id",
603+
msg: "Here is a thread",
604+
});
605+
jest.spyOn(threadRoot, "isThreadRoot", "get").mockReturnValue(true);
606+
607+
const message = TestUtilsMatrix.mkMessage({
608+
event: true,
609+
room: "!room:id",
610+
user: "@user:id",
611+
msg: "And here's another message in the main timeline",
612+
});
613+
614+
expect(shouldFormContinuation(threadRoot, message, false, true)).toEqual(false);
615+
expect(shouldFormContinuation(message, threadRoot, false, true)).toEqual(true);
616+
});
617+
});

test/test-utils/test-utils.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,20 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent {
191191
].indexOf(opts.type) !== -1) {
192192
event.state_key = "";
193193
}
194-
return opts.event ? new MatrixEvent(event) : event as unknown as MatrixEvent;
194+
195+
const mxEvent = opts.event ? new MatrixEvent(event) : event as unknown as MatrixEvent;
196+
if (!mxEvent.sender && opts.user && opts.room) {
197+
mxEvent.sender = {
198+
userId: opts.user,
199+
membership: "join",
200+
name: opts.user,
201+
rawDisplayName: opts.user,
202+
roomId: opts.room,
203+
getAvatarUrl: () => {},
204+
getMxcAvatarUrl: () => {},
205+
} as unknown as RoomMember;
206+
}
207+
return mxEvent;
195208
}
196209

197210
/**

0 commit comments

Comments
 (0)