Allow jumping to message search from spotlight (#29850)

* Allow jumping to message search from spotlight

replaces the message search hint which referenced the old UX

Fixes #29831

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update RoomSummaryCard.tsx

* Update actions.ts

* Delete src/hooks/useTransition.ts

* Update RoomSummaryCard.tsx

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-04-30 12:23:35 +01:00
committed by GitHub
parent 23597e959b
commit 4bf28f8159
14 changed files with 212 additions and 240 deletions

View File

@@ -309,6 +309,8 @@ export function createTestClient(): MatrixClient {
pushProcessor: {
getPushRuleById: jest.fn(),
},
search: jest.fn().mockResolvedValue({}),
processRoomEventsSearch: jest.fn().mockResolvedValue({ highlights: [], results: [] }),
} as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client);

View File

@@ -608,129 +608,148 @@ describe("RoomView", () => {
});
});
it("should close search results when edit is clicked", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
describe("message search", () => {
it("should close search results when edit is clicked", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.Room,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.Room,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room.roomId,
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
}),
);
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = waitForElementToBeRemoved(() => container.querySelector(".mx_RoomView_searchResultsPanel"));
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await prom;
});
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
rooms.set(room2.roomId, room2);
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.All,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
context: {
events_before: [],
events_after: [],
profile_info: {},
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room2.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
}),
inProgress: false,
count: 1,
},
}),
);
},
}),
);
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = waitForElementToBeRemoved(() => container.querySelector(".mx_RoomView_searchResultsPanel"));
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await prom;
});
const prom = untilDispatch(Action.ViewRoom, defaultDispatcher);
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
it("should switch rooms when edit is clicked on a search result for a different room", async () => {
const room2 = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
rooms.set(room2.roomId, room2);
await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId }));
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const eventMapper = (obj: Partial<IEvent>) => new MatrixEvent(obj);
const roomViewRef = createRef<RoomView>();
const { container, getByText, findByLabelText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
// @ts-ignore - triggering a search organically is a lot of work
act(() =>
roomViewRef.current!.setState({
search: {
searchId: 1,
roomId: room.roomId,
term: "search term",
scope: SearchScope.All,
promise: Promise.resolve({
results: [
SearchResult.fromJson(
{
rank: 1,
result: {
content: {
body: "search term",
msgtype: "m.text",
},
type: "m.room.message",
event_id: "$eventId",
sender: cli.getSafeUserId(),
origin_server_ts: 123456789,
room_id: room2.roomId,
},
context: {
events_before: [],
events_after: [],
profile_info: {},
},
},
eventMapper,
),
],
highlights: [],
count: 1,
}),
inProgress: false,
count: 1,
},
}),
);
await waitFor(() => {
expect(container.querySelector(".mx_RoomView_searchResultsPanel")).toBeVisible();
});
const prom = untilDispatch(Action.ViewRoom, defaultDispatcher);
await userEvent.hover(getByText("search term"));
await userEvent.click(await findByLabelText("Edit"));
await expect(prom).resolves.toEqual(expect.objectContaining({ room_id: room2.roomId }));
});
it("should pre-fill search field on FocusMessageSearch dispatch", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
const roomViewRef = createRef<RoomView>();
const { findByPlaceholderText } = await mountRoomView(roomViewRef);
await waitFor(() => expect(roomViewRef.current).toBeTruthy());
act(() =>
defaultDispatcher.dispatch({
action: Action.FocusMessageSearch,
initialText: "search term",
}),
);
await expect(findByPlaceholderText("Search messages…")).resolves.toHaveValue("search term");
});
});
it("fires Action.RoomLoaded", async () => {

View File

@@ -353,12 +353,12 @@ describe("Spotlight Dialog", () => {
});
it("should find Rooms", () => {
expect(options).toHaveLength(4);
expect(options).toHaveLength(5);
expect(options[0]!.innerHTML).toContain(testRoom.name);
});
it("should not find LocalRooms", () => {
expect(options).toHaveLength(4);
expect(options).toHaveLength(5);
expect(options[0]!.innerHTML).not.toContain(testLocalRoom.name);
});
});
@@ -648,4 +648,20 @@ describe("Spotlight Dialog", () => {
});
});
});
it("should allow jumping into message search", async () => {
const onFinished = jest.fn();
render(<SpotlightDialog initialText="search term" onFinished={onFinished} />);
jest.advanceTimersByTime(200);
await flushPromisesWithFakeTimers();
fireEvent.click(screen.getByText("Messages"));
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
action: Action.FocusMessageSearch,
initialText: "search term",
}),
);
});
});

View File

@@ -11,7 +11,6 @@ import { render, fireEvent, screen, waitFor } from "jest-matrix-react";
import { EventType, MatrixEvent, Room, type MatrixClient, JoinRule } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked, type MockedObject } from "jest-mock";
import userEvent from "@testing-library/user-event";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import RoomSummaryCard from "../../../../../src/components/views/right_panel/RoomSummaryCard";
@@ -30,8 +29,6 @@ import { _t } from "../../../../../src/languageHandler";
import { tagRoom } from "../../../../../src/utils/room/tagRoom";
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
import { Action } from "../../../../../src/dispatcher/actions";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import { ReportRoomDialog } from "../../../../../src/components/views/dialogs/ReportRoomDialog.tsx";
jest.mock("../../../../../src/utils/room/tagRoom");
@@ -169,38 +166,6 @@ describe("<RoomSummaryCard />", () => {
fireEvent.keyDown(getByPlaceholderText("Search messages…"), { key: "Escape" });
expect(onSearchCancel).toHaveBeenCalled();
});
it("should empty search field when the timeline rendering type changes away", async () => {
const onSearchChange = jest.fn();
const { rerender } = render(
<MatrixClientContext.Provider value={mockClient}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Search } as any)}>
<RoomSummaryCard
room={room}
permalinkCreator={new RoomPermalinkCreator(room)}
onSearchChange={onSearchChange}
focusRoomSearch={true}
/>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
await userEvent.type(screen.getByPlaceholderText("Search messages…"), "test");
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("test");
rerender(
<MatrixClientContext.Provider value={mockClient}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Room } as any)}>
<RoomSummaryCard
room={room}
permalinkCreator={new RoomPermalinkCreator(room)}
onSearchChange={onSearchChange}
/>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("");
});
});
it("opens room file panel on button click", () => {