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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
|
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
|
||||||
import { Thread } from "matrix-js-sdk/src/models/thread";
|
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 [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
|
||||||
const [room, setRoom] = useState<Room | null>(null);
|
const [room, setRoom] = useState<Room | null>(null);
|
||||||
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
|
|
||||||
const [narrow, setNarrow] = useState<boolean>(false);
|
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(() => {
|
useEffect(() => {
|
||||||
const room = mxClient.getRoom(roomId);
|
const room = mxClient.getRoom(roomId);
|
||||||
room.createThreadsTimelineSets()
|
room?.createThreadsTimelineSets()
|
||||||
.then(() => {
|
.then(() => room.fetchRoomThreads())
|
||||||
return room.fetchRoomThreads();
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setFilterOption(ThreadFilterType.All);
|
setFilterOption(ThreadFilterType.All);
|
||||||
setRoom(room);
|
setRoom(room);
|
||||||
});
|
});
|
||||||
}, [mxClient, roomId]);
|
}, [mxClient, roomId]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (room) {
|
|
||||||
if (filterOption === ThreadFilterType.My) {
|
|
||||||
setTimelineSet(room.threadsTimelineSets[1]);
|
|
||||||
} else {
|
|
||||||
setTimelineSet(room.threadsTimelineSets[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [room, filterOption]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timelineSet && !Thread.hasServerSideSupport) {
|
if (timelineSet && !Thread.hasServerSideSupport) {
|
||||||
timelinePanel.current.refreshTimeline();
|
timelinePanel.current.refreshTimeline();
|
||||||
|
@ -268,7 +260,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader
|
||||||
filterOption={filterOption}
|
filterOption={filterOption}
|
||||||
setFilterOption={setFilterOption}
|
setFilterOption={setFilterOption}
|
||||||
empty={!timelineSet?.getLiveTimeline()?.getEvents().length}
|
empty={!hasThreads}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
|
@ -315,7 +307,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
showUrlPreview={false} // No URL previews at the threads list level
|
showUrlPreview={false} // No URL previews at the threads list level
|
||||||
empty={
|
empty={
|
||||||
<EmptyThread
|
<EmptyThread
|
||||||
hasThreads={room.threadsTimelineSets?.[0]?.getLiveTimeline().getEvents().length > 0}
|
hasThreads={hasThreads}
|
||||||
filterOption={filterOption}
|
filterOption={filterOption}
|
||||||
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
|
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,18 +14,23 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
import "focus-visible"; // to fix context menus
|
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 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 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");
|
jest.mock("../../../src/utils/Feedback");
|
||||||
|
|
||||||
|
@ -122,4 +127,185 @@ describe("ThreadPanel", () => {
|
||||||
expect(foundButton).toMatchSnapshot();
|
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