mirror of https://github.com/vector-im/riot-web
Fix issue where thread dropdown would not show up correctly (#9872)
* Fix issue where thread dropdown would not correctly * Write additional test for both issues - Thread dropdown should be shown if there is any thread, even if not participated - Thread list correctly updates after every change of the dropdown immediatelypull/28788/head^2
parent
1b5f06b16f
commit
7a36ba0fde
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
|
||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||
|
@ -215,31 +216,22 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
|
||||
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
|
||||
const [room, setRoom] = useState<Room | null>(null);
|
||||
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
|
||||
const [narrow, setNarrow] = useState<boolean>(false);
|
||||
|
||||
const timelineSet: Optional<EventTimelineSet> =
|
||||
filterOption === ThreadFilterType.My ? room?.threadsTimelineSets[1] : room?.threadsTimelineSets[0];
|
||||
const hasThreads = Boolean(room?.threadsTimelineSets?.[0]?.getLiveTimeline()?.getEvents()?.length);
|
||||
|
||||
useEffect(() => {
|
||||
const room = mxClient.getRoom(roomId);
|
||||
room.createThreadsTimelineSets()
|
||||
.then(() => {
|
||||
return room.fetchRoomThreads();
|
||||
})
|
||||
room?.createThreadsTimelineSets()
|
||||
.then(() => room.fetchRoomThreads())
|
||||
.then(() => {
|
||||
setFilterOption(ThreadFilterType.All);
|
||||
setRoom(room);
|
||||
});
|
||||
}, [mxClient, roomId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (room) {
|
||||
if (filterOption === ThreadFilterType.My) {
|
||||
setTimelineSet(room.threadsTimelineSets[1]);
|
||||
} else {
|
||||
setTimelineSet(room.threadsTimelineSets[0]);
|
||||
}
|
||||
}
|
||||
}, [room, filterOption]);
|
||||
|
||||
useEffect(() => {
|
||||
if (timelineSet && !Thread.hasServerSideSupport) {
|
||||
timelinePanel.current.refreshTimeline();
|
||||
|
@ -268,7 +260,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
<ThreadPanelHeader
|
||||
filterOption={filterOption}
|
||||
setFilterOption={setFilterOption}
|
||||
empty={!timelineSet?.getLiveTimeline()?.getEvents().length}
|
||||
empty={!hasThreads}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
|
@ -315,7 +307,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
showUrlPreview={false} // No URL previews at the threads list level
|
||||
empty={
|
||||
<EmptyThread
|
||||
hasThreads={room.threadsTimelineSets?.[0]?.getLiveTimeline().getEvents().length > 0}
|
||||
hasThreads={hasThreads}
|
||||
filterOption={filterOption}
|
||||
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
|
||||
/>
|
||||
|
|
|
@ -14,18 +14,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import "focus-visible"; // to fix context menus
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
|
||||
import React from "react";
|
||||
|
||||
import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../src/components/structures/ThreadPanel";
|
||||
import { _t } from "../../../src/languageHandler";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
|
||||
import { createTestClient, mkStubRoom } from "../../test-utils";
|
||||
import { shouldShowFeedback } from "../../../src/utils/Feedback";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../src/contexts/RoomContext";
|
||||
import { _t } from "../../../src/languageHandler";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { shouldShowFeedback } from "../../../src/utils/Feedback";
|
||||
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
|
||||
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
|
||||
import { createTestClient, getRoomContext, mkStubRoom, mockPlatformPeg, stubClient } from "../../test-utils";
|
||||
import { mkThread } from "../../test-utils/threads";
|
||||
|
||||
jest.mock("../../../src/utils/Feedback");
|
||||
|
||||
|
@ -122,4 +127,185 @@ describe("ThreadPanel", () => {
|
|||
expect(foundButton).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Filtering", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
const SENDER = "@alice:example.org";
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const TestThreadPanel = () => (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomContext.Provider
|
||||
value={getRoomContext(room, {
|
||||
canSendMessages: true,
|
||||
})}
|
||||
>
|
||||
<ThreadPanel
|
||||
roomId={ROOM_ID}
|
||||
onClose={jest.fn()}
|
||||
resizeNotifier={new ResizeNotifier()}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockPlatformPeg();
|
||||
mockClient = mocked(MatrixClientPeg.get());
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true);
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
jest.spyOn(room, "fetchRoomThreads").mockReturnValue(Promise.resolve());
|
||||
jest.spyOn(mockClient, "getRoom").mockReturnValue(room);
|
||||
await room.createThreadsTimelineSets();
|
||||
const [allThreads, myThreads] = room.threadsTimelineSets;
|
||||
jest.spyOn(room, "createThreadsTimelineSets").mockReturnValue(Promise.resolve([allThreads, myThreads]));
|
||||
});
|
||||
|
||||
function toggleThreadFilter(container: HTMLElement, newFilter: ThreadFilterType) {
|
||||
fireEvent.click(container.querySelector(".mx_ThreadPanel_dropdown")!);
|
||||
const found = screen.queryAllByRole("menuitemradio");
|
||||
expect(found).toHaveLength(2);
|
||||
|
||||
const allThreadsContent = `${_t("All threads")}${_t("Shows all threads from current room")}`;
|
||||
const myThreadsContent = `${_t("My threads")}${_t("Shows all threads you've participated in")}`;
|
||||
|
||||
const allThreadsOption = found.find((it) => it.textContent === allThreadsContent);
|
||||
const myThreadsOption = found.find((it) => it.textContent === myThreadsContent);
|
||||
expect(allThreadsOption).toBeTruthy();
|
||||
expect(myThreadsOption).toBeTruthy();
|
||||
|
||||
const toSelect = newFilter === ThreadFilterType.My ? myThreadsOption : allThreadsOption;
|
||||
fireEvent.click(toSelect!);
|
||||
}
|
||||
|
||||
type EventData = { sender: string | null; content: string | null };
|
||||
|
||||
function findEvents(container: HTMLElement): EventData[] {
|
||||
return Array.from(container.querySelectorAll(".mx_EventTile")).map((el) => {
|
||||
const sender = el.querySelector(".mx_DisambiguatedProfile_displayName")?.textContent ?? null;
|
||||
const content = el.querySelector(".mx_EventTile_body")?.textContent ?? null;
|
||||
return { sender, content };
|
||||
});
|
||||
}
|
||||
|
||||
function toEventData(event: MatrixEvent): EventData {
|
||||
return { sender: event.event.sender ?? null, content: event.event.content?.body ?? null };
|
||||
}
|
||||
|
||||
it("correctly filters Thread List with multiple threads", async () => {
|
||||
const otherThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: SENDER,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const mixedThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: SENDER,
|
||||
participantUserIds: [SENDER, mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const ownThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: mockClient.getUserId()!,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const threadRoots = [otherThread.rootEvent, mixedThread.rootEvent, ownThread.rootEvent];
|
||||
jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation((_, eventId) => {
|
||||
const event = threadRoots.find((it) => it.getId() === eventId)?.event;
|
||||
return event ? Promise.resolve(event) : Promise.reject();
|
||||
});
|
||||
const [allThreads, myThreads] = room.threadsTimelineSets;
|
||||
allThreads.addLiveEvent(otherThread.rootEvent);
|
||||
allThreads.addLiveEvent(mixedThread.rootEvent);
|
||||
allThreads.addLiveEvent(ownThread.rootEvent);
|
||||
myThreads.addLiveEvent(mixedThread.rootEvent);
|
||||
myThreads.addLiveEvent(ownThread.rootEvent);
|
||||
|
||||
let events: EventData[] = [];
|
||||
const renderResult = render(<TestThreadPanel />);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(3);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
|
||||
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.My);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(2);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(mixedThread.rootEvent));
|
||||
expect(events[1]).toEqual(toEventData(ownThread.rootEvent));
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.All);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(3);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
expect(events[1]).toEqual(toEventData(mixedThread.rootEvent));
|
||||
expect(events[2]).toEqual(toEventData(ownThread.rootEvent));
|
||||
});
|
||||
|
||||
it("correctly filters Thread List with a single, unparticipated thread", async () => {
|
||||
const otherThread = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: SENDER,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
const threadRoots = [otherThread.rootEvent];
|
||||
jest.spyOn(mockClient, "fetchRoomEvent").mockImplementation((_, eventId) => {
|
||||
const event = threadRoots.find((it) => it.getId() === eventId)?.event;
|
||||
return event ? Promise.resolve(event) : Promise.reject();
|
||||
});
|
||||
const [allThreads] = room.threadsTimelineSets;
|
||||
allThreads.addLiveEvent(otherThread.rootEvent);
|
||||
|
||||
let events: EventData[] = [];
|
||||
const renderResult = render(<TestThreadPanel />);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(1);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_ThreadPanel_dropdown")).toBeTruthy());
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.My);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(0);
|
||||
});
|
||||
toggleThreadFilter(renderResult.container, ThreadFilterType.All);
|
||||
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
|
||||
await waitFor(() => {
|
||||
events = findEvents(renderResult.container);
|
||||
expect(findEvents(renderResult.container)).toHaveLength(1);
|
||||
});
|
||||
expect(events[0]).toEqual(toEventData(otherThread.rootEvent));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue