Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 9136a58

Browse files
authored
Prevent re-filtering user directory results in spotlight (#11290)
* Prevent re-filtering user directory results in spotlight As they were already filtered by the server and may be fuzzier than any filtering we can do locally, e.g. matching against email addresses or other fields not available to the client * deduplicate work * Improve coverage
1 parent d9aaed0 commit 9136a58

File tree

2 files changed

+83
-22
lines changed

2 files changed

+83
-22
lines changed

src/components/views/dialogs/spotlight/SpotlightDialog.tsx

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ interface IRoomResult extends IBaseResult {
142142

143143
interface IMemberResult extends IBaseResult {
144144
member: Member | RoomMember;
145+
/**
146+
* If the result is from a filtered server API then we set true here to avoid locally culling it in our own filters
147+
*/
148+
alreadyFiltered: boolean;
145149
}
146150

147151
interface IResult extends IBaseResult {
@@ -201,7 +205,8 @@ const toRoomResult = (room: Room): IRoomResult => {
201205
}
202206
};
203207

204-
const toMemberResult = (member: Member | RoomMember): IMemberResult => ({
208+
const toMemberResult = (member: Member | RoomMember, alreadyFiltered: boolean): IMemberResult => ({
209+
alreadyFiltered,
205210
member,
206211
section: Section.Suggestions,
207212
filter: [Filter.People],
@@ -240,13 +245,9 @@ const findVisibleRooms = (cli: MatrixClient, msc3946ProcessDynamicPredecessor: b
240245
});
241246
};
242247

243-
const findVisibleRoomMembers = (
244-
cli: MatrixClient,
245-
msc3946ProcessDynamicPredecessor: boolean,
246-
filterDMs = true,
247-
): RoomMember[] => {
248+
const findVisibleRoomMembers = (visibleRooms: Room[], cli: MatrixClient, filterDMs = true): RoomMember[] => {
248249
return Object.values(
249-
findVisibleRooms(cli, msc3946ProcessDynamicPredecessor)
250+
visibleRooms
250251
.filter((room) => !filterDMs || !DMRoomMap.shared().getUserIdForRoomId(room.roomId))
251252
.reduce((members, room) => {
252253
for (const member of room.getJoinedMembers()) {
@@ -331,23 +332,40 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
331332
useDebouncedCallback(filter === Filter.People, searchProfileInfo, searchParams);
332333

333334
const possibleResults = useMemo<Result[]>(() => {
335+
const visibleRooms = findVisibleRooms(cli, msc3946ProcessDynamicPredecessor);
336+
const roomResults = visibleRooms.map(toRoomResult);
334337
const userResults: IMemberResult[] = [];
335-
const roomResults = findVisibleRooms(cli, msc3946ProcessDynamicPredecessor).map(toRoomResult);
336-
// If we already have a DM with the user we're looking for, we will
337-
// show that DM instead of the user themselves
338+
339+
// If we already have a DM with the user we're looking for, we will show that DM instead of the user themselves
338340
const alreadyAddedUserIds = roomResults.reduce((userIds, result) => {
339341
const userId = DMRoomMap.shared().getUserIdForRoomId(result.room.roomId);
340342
if (!userId) return userIds;
341343
if (result.room.getJoinedMemberCount() > 2) return userIds;
342-
userIds.add(userId);
344+
userIds.set(userId, result);
343345
return userIds;
344-
}, new Set<string>());
345-
for (const user of [...findVisibleRoomMembers(cli, msc3946ProcessDynamicPredecessor), ...users]) {
346-
// Make sure we don't have any user more than once
347-
if (alreadyAddedUserIds.has(user.userId)) continue;
348-
alreadyAddedUserIds.add(user.userId);
349-
350-
userResults.push(toMemberResult(user));
346+
}, new Map<string, IMemberResult | IRoomResult>());
347+
348+
function addUserResults(users: Array<Member | RoomMember>, alreadyFiltered: boolean): void {
349+
for (const user of users) {
350+
// Make sure we don't have any user more than once
351+
if (alreadyAddedUserIds.has(user.userId)) {
352+
const result = alreadyAddedUserIds.get(user.userId)!;
353+
if (alreadyFiltered && isMemberResult(result) && !result.alreadyFiltered) {
354+
// But if they were added as not yet filtered then mark them as already filtered to avoid
355+
// culling this result based on local filtering.
356+
result.alreadyFiltered = true;
357+
}
358+
continue;
359+
}
360+
const result = toMemberResult(user, alreadyFiltered);
361+
alreadyAddedUserIds.set(user.userId, result);
362+
userResults.push(result);
363+
}
364+
}
365+
addUserResults(findVisibleRoomMembers(visibleRooms, cli), false);
366+
addUserResults(users, true);
367+
if (profile) {
368+
addUserResults([new DirectoryMember(profile)], true);
351369
}
352370

353371
return [
@@ -369,9 +387,6 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
369387
})),
370388
...roomResults,
371389
...userResults,
372-
...(profile && !alreadyAddedUserIds.has(profile.user_id) ? [new DirectoryMember(profile)] : []).map(
373-
toMemberResult,
374-
),
375390
...publicRooms.map(toPublicRoomResult),
376391
].filter((result) => filter === null || result.filter.includes(filter));
377392
}, [cli, users, profile, publicRooms, filter, msc3946ProcessDynamicPredecessor]);
@@ -399,7 +414,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
399414
)
400415
return; // bail, does not match query
401416
} else if (isMemberResult(entry)) {
402-
if (!entry.query?.some((q) => q.includes(lcQuery))) return; // bail, does not match query
417+
if (!entry.alreadyFiltered && !entry.query?.some((q) => q.includes(lcQuery))) return; // bail, does not match query
403418
} else if (isPublicRoomResult(entry)) {
404419
if (!entry.query?.some((q) => q.includes(lcQuery))) return; // bail, does not match query
405420
} else {

test/components/views/dialogs/SpotlightDialog-test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,52 @@ describe("Spotlight Dialog", () => {
338338
});
339339
});
340340

341+
it("should not filter out users sent by the server", async () => {
342+
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
343+
results: [
344+
{ user_id: "@user1:server", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
345+
{ user_id: "@user2:server", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
346+
],
347+
limited: false,
348+
});
349+
350+
render(<SpotlightDialog initialFilter={Filter.People} initialText="Alpha" onFinished={() => null} />);
351+
// search is debounced
352+
jest.advanceTimersByTime(200);
353+
await flushPromisesWithFakeTimers();
354+
355+
const content = document.querySelector("#mx_SpotlightDialog_content")!;
356+
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
357+
expect(options.length).toBeGreaterThanOrEqual(2);
358+
expect(options[0]).toHaveTextContent("User Alpha");
359+
expect(options[1]).toHaveTextContent("User Beta");
360+
});
361+
362+
it("should not filter out users sent by the server even if a local suggestion gets filtered out", async () => {
363+
const member = new RoomMember(testRoom.roomId, testPerson.user_id);
364+
member.name = member.rawDisplayName = testPerson.display_name!;
365+
member.getMxcAvatarUrl = jest.fn().mockReturnValue("mxc://0/avatar");
366+
mocked(testRoom.getJoinedMembers).mockReturnValue([member]);
367+
mocked(mockedClient.searchUserDirectory).mockResolvedValue({
368+
results: [
369+
{ user_id: "@janedoe:matrix.org", display_name: "User Alpha", avatar_url: "mxc://1/avatar" },
370+
{ user_id: "@johndoe:matrix.org", display_name: "User Beta", avatar_url: "mxc://2/avatar" },
371+
],
372+
limited: false,
373+
});
374+
375+
render(<SpotlightDialog initialFilter={Filter.People} initialText="Beta" onFinished={() => null} />);
376+
// search is debounced
377+
jest.advanceTimersByTime(200);
378+
await flushPromisesWithFakeTimers();
379+
380+
const content = document.querySelector("#mx_SpotlightDialog_content")!;
381+
const options = content.querySelectorAll("li.mx_SpotlightDialog_option");
382+
expect(options.length).toBeGreaterThanOrEqual(2);
383+
expect(options[0]).toHaveTextContent(testPerson.display_name!);
384+
expect(options[1]).toHaveTextContent("User Beta");
385+
});
386+
341387
it("should start a DM when clicking a person", async () => {
342388
render(
343389
<SpotlightDialog

0 commit comments

Comments
 (0)