From 86034b4b016a71bbcd31966fa3a62c38302eff7b Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 20 Dec 2022 17:07:03 +0100 Subject: [PATCH 1/5] Fix blank timeline when thread root is UTD --- src/components/structures/TimelinePanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 214e252fbcb..04eca0a8b35 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1637,7 +1637,9 @@ class TimelinePanel extends React.Component { let i = events.length - 1; let userMembership = "leave"; for (; i >= 0; i--) { - const timeline = room.getTimelineForEvent(events[i].getId()); + const timeline = isThreadTimeline + ? room.getTimelineForEvent(events[i].getId()!) + : room.getUnfilteredTimelineSet().getTimelineForEvent(events[i].getId()!); if (!timeline) { // Somehow, it seems to be possible for live events to not have // a timeline, even though that should not happen. :( From 65aaec485065e53ff0ac6ab919e6ebe98f9260c4 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 21 Dec 2022 10:03:03 +0100 Subject: [PATCH 2/5] add test for pre join uisi checks and thread roots --- src/components/structures/TimelinePanel.tsx | 4 +- .../structures/TimelinePanel-test.tsx | 67 ++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 04eca0a8b35..c1fc13bbb19 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1637,9 +1637,7 @@ class TimelinePanel extends React.Component { let i = events.length - 1; let userMembership = "leave"; for (; i >= 0; i--) { - const timeline = isThreadTimeline - ? room.getTimelineForEvent(events[i].getId()!) - : room.getUnfilteredTimelineSet().getTimelineForEvent(events[i].getId()!); + const timeline = this.props.timelineSet.getTimelineForEvent(events[i].getId()!); if (!timeline) { // Somehow, it seems to be possible for live events to not have // a timeline, even though that should not happen. :( diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index cf1a5188461..4731ff502f1 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -27,6 +27,8 @@ import { PendingEventOrdering, Room, RoomEvent, + RoomMember, + RoomState, TimelineWindow, } from "matrix-js-sdk/src/matrix"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; @@ -44,7 +46,8 @@ import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import SettingsStore from "../../../src/settings/SettingsStore"; import { isCallEvent } from "../../../src/components/structures/LegacyCallEventGrouper"; -import { flushPromises, mkRoom, stubClient } from "../../test-utils"; +import { flushPromises, mkMembership, mkRoom, stubClient } from "../../test-utils"; +import { mkThread } from "../../test-utils/threads"; const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent => { const receiptContent = { @@ -58,7 +61,7 @@ const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs }; const getProps = (room: Room, events: MatrixEvent[]): TimelinePanel["props"] => { - const timelineSet = { room: room as Room } as EventTimelineSet; + const timelineSet = room.getUnfilteredTimelineSet?.() ?? ({ room: room as Room } as EventTimelineSet); const timeline = new EventTimeline(timelineSet); events.forEach((event) => timeline.addEvent(event, true)); timelineSet.getLiveTimeline = () => timeline; @@ -513,4 +516,64 @@ describe("TimelinePanel", () => { replyToEvent.mockClear(); }); }); + + it.only("renders when the last message is an undecryptable thread root", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_threadstable"); + + const client = MatrixClientPeg.get(); + client.isRoomEncrypted = () => true; + client.supportsExperimentalThreads = () => true; + client.decryptEventIfNeeded = () => Promise.resolve(); + const authorId = client.getUserId(); + const room = new Room("roomId", client, authorId, { + lazyLoadMembers: false, + }); + + const events = mockEvents(room); + const props = { + ...getProps(room, events), + }; + + const { rootEvent } = mkThread({ + room, + client, + authorId, + participantUserIds: [authorId], + }); + + events.push(rootEvent); + + const roomMembership = mkMembership({ + mship: "join", + prevMship: "join", + user: authorId, + room: room.roomId, + event: true, + }); + roomMembership.event.state_key = "123"; + + events.push(roomMembership); + + const member = new RoomMember(room.roomId, authorId); + member.membership = "join"; + + const roomState = new RoomState(room.roomId); + jest.spyOn(roomState, "getMember").mockReturnValue(member); + + jest.spyOn(props.timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState); + props.timelineSet.addLiveEvent(roomMembership, {}); + + for (const event of events) { + jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true); + jest.spyOn(event, "shouldAttemptDecryption").mockReturnValue(false); + } + + const { container } = render( + + + , + ); + + expect(container.querySelectorAll(".mx_EventTile")).toHaveLength(5); + }); }); From 661fdd9e54f418790b72827afaade279a24fb754 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 21 Dec 2022 10:12:13 +0100 Subject: [PATCH 3/5] ts strict fix --- test/components/structures/TimelinePanel-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index 4731ff502f1..df0289e3266 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -524,7 +524,7 @@ describe("TimelinePanel", () => { client.isRoomEncrypted = () => true; client.supportsExperimentalThreads = () => true; client.decryptEventIfNeeded = () => Promise.resolve(); - const authorId = client.getUserId(); + const authorId = client.getUserId()!; const room = new Room("roomId", client, authorId, { lazyLoadMembers: false, }); From be2871f14dd2e2f41d198f1aa191f92e36263641 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 21 Dec 2022 12:31:15 +0000 Subject: [PATCH 4/5] Update to timeline panel test Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- test/components/structures/TimelinePanel-test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index 40db7633ad3..c0761995650 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -530,9 +530,7 @@ describe("TimelinePanel", () => { }); const events = mockEvents(room); - const props = { - ...getProps(room, events), - }; + const props = getProps(room, events); const { rootEvent } = mkThread({ room, @@ -549,8 +547,8 @@ describe("TimelinePanel", () => { user: authorId, room: room.roomId, event: true, + skey: "123", }); - roomMembership.event.state_key = "123"; events.push(roomMembership); From 50d45bd3ccf2c373fb7b5f26479e1482ae75ec27 Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 21 Dec 2022 13:57:33 +0100 Subject: [PATCH 5/5] Fixes to TimelinePanel-test --- .../structures/TimelinePanel-test.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index c0761995650..d7ff659c9d1 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { render, RenderResult } from "@testing-library/react"; +import { render, RenderResult, waitFor, screen } from "@testing-library/react"; // eslint-disable-next-line deprecate/import import { mount, ReactWrapper } from "enzyme"; import { MessageEvent } from "matrix-events-sdk"; @@ -61,9 +61,9 @@ const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs }; const getProps = (room: Room, events: MatrixEvent[]): TimelinePanel["props"] => { - const timelineSet = room.getUnfilteredTimelineSet?.() ?? ({ room: room as Room } as EventTimelineSet); + const timelineSet = { room: room as Room } as EventTimelineSet; const timeline = new EventTimeline(timelineSet); - events.forEach((event) => timeline.addEvent(event, true)); + events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: true })); timelineSet.getLiveTimeline = () => timeline; timelineSet.getTimelineForEvent = () => timeline; timelineSet.getPendingEvents = () => events; @@ -433,7 +433,7 @@ describe("TimelinePanel", () => { // @ts-ignore thread.fetchEditsWhereNeeded = () => Promise.resolve(); await thread.addEvent(reply1, true); - await allThreads.getLiveTimeline().addEvent(thread.rootEvent!, true); + await allThreads.getLiveTimeline().addEvent(thread.rootEvent!, { toStartOfTimeline: true }); const replyToEvent = jest.spyOn(thread, "replyToEvent", "get"); const dom = render( @@ -479,7 +479,7 @@ describe("TimelinePanel", () => { // @ts-ignore realThread.fetchEditsWhereNeeded = () => Promise.resolve(); await realThread.addEvent(reply1, true); - await allThreads.getLiveTimeline().addEvent(realThread.rootEvent!, true); + await allThreads.getLiveTimeline().addEvent(realThread.rootEvent!, { toStartOfTimeline: true }); const replyToEvent = jest.spyOn(realThread, "replyToEvent", "get"); // @ts-ignore @@ -517,7 +517,7 @@ describe("TimelinePanel", () => { }); }); - it("renders when the last message is an undecryptable thread root", () => { + it("renders when the last message is an undecryptable thread root", async () => { jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => name === "feature_threadstable"); const client = MatrixClientPeg.get(); @@ -527,10 +527,11 @@ describe("TimelinePanel", () => { const authorId = client.getUserId()!; const room = new Room("roomId", client, authorId, { lazyLoadMembers: false, + pendingEventOrdering: PendingEventOrdering.Detached, }); const events = mockEvents(room); - const props = getProps(room, events); + const timelineSet = room.getUnfilteredTimelineSet(); const { rootEvent } = mkThread({ room, @@ -541,6 +542,8 @@ describe("TimelinePanel", () => { events.push(rootEvent); + events.forEach((event) => timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true })); + const roomMembership = mkMembership({ mship: "join", prevMship: "join", @@ -558,8 +561,8 @@ describe("TimelinePanel", () => { const roomState = new RoomState(room.roomId); jest.spyOn(roomState, "getMember").mockReturnValue(member); - jest.spyOn(props.timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState); - props.timelineSet.addLiveEvent(roomMembership, {}); + jest.spyOn(timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState); + timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { toStartOfTimeline: false }); for (const event of events) { jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true); @@ -568,10 +571,11 @@ describe("TimelinePanel", () => { const { container } = render( - + , ); - expect(container.querySelectorAll(".mx_EventTile")).toHaveLength(5); + await waitFor(() => expect(screen.queryByRole("progressbar")).toBeNull()); + await waitFor(() => expect(container.querySelector(".mx_RoomView_MessageList")).not.toBeEmptyDOMElement()); }); });