diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 05ab8c1749a..a9aae28d571 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -47,6 +47,7 @@ import defaultDispatcher from "../../../../dispatcher/dispatcher"; import { ViewRoomPayload } from "../../../../dispatcher/payloads/ViewRoomPayload"; import { useDebouncedCallback } from "../../../../hooks/spotlight/useDebouncedCallback"; import { useRecentSearches } from "../../../../hooks/spotlight/useRecentSearches"; +import { useWebSearchMetrics } from "../../../../hooks/spotlight/useWebSearchMetrics"; import { useProfileInfo } from "../../../../hooks/useProfileInfo"; import { usePublicRoomDirectory } from "../../../../hooks/usePublicRoomDirectory"; import { useSpaceResults } from "../../../../hooks/useSpaceResults"; @@ -54,7 +55,6 @@ import { useUserDirectory } from "../../../../hooks/useUserDirectory"; import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; import { _t } from "../../../../languageHandler"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; -import { PosthogAnalytics } from "../../../../PosthogAnalytics"; import { getCachedRoomIDForAlias } from "../../../../RoomAliasCache"; import { showStartChatInviteDialog } from "../../../../RoomInvite"; import { SettingLevel } from "../../../../settings/SettingLevel"; @@ -219,26 +219,6 @@ const toMemberResult = (member: Member | RoomMember): IMemberResult => ({ const recentAlgorithm = new RecentAlgorithm(); -export const useWebSearchMetrics = (numResults: number, queryLength: number, viaSpotlight: boolean): void => { - useEffect(() => { - if (!queryLength) return; - - // send metrics after a 1s debounce - const timeoutId = window.setTimeout(() => { - PosthogAnalytics.instance.trackEvent({ - eventName: "WebSearch", - viaSpotlight, - numResults, - queryLength, - }); - }, 1000); - - return () => { - clearTimeout(timeoutId); - }; - }, [numResults, queryLength, viaSpotlight]); -}; - const findVisibleRooms = (cli: MatrixClient, msc3946ProcessDynamicPredecessor: boolean): Room[] => { return cli.getVisibleRooms(msc3946ProcessDynamicPredecessor).filter((room) => { // Do not show local rooms @@ -351,7 +331,12 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n userIds.add(userId); return userIds; }, new Set()); - for (const user of [...findVisibleRoomMembers(cli, msc3946ProcessDynamicPredecessor), ...users]) { + + const lcQuery = trimmedQuery.toLowerCase(); + const members = findVisibleRoomMembers(cli); + const localUsers = members.filter((localResult) => [localResult.name, localResult.userId].some((q) => q.includes(lcQuery))); + + for (const user of [...localUsers, ...users]) { // Make sure we don't have any user more than once if (alreadyAddedUserIds.has(user.userId)) continue; alreadyAddedUserIds.add(user.userId); @@ -383,7 +368,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n ), ...publicRooms.map(toPublicRoomResult), ].filter((result) => filter === null || result.filter.includes(filter)); - }, [cli, users, profile, publicRooms, filter, msc3946ProcessDynamicPredecessor]); + }, [cli, users, profile, publicRooms, filter, msc3946ProcessDynamicPredecessor, trimmedQuery]); const results = useMemo>(() => { const results: Record = { @@ -408,7 +393,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n ) return; // bail, does not match query } else if (isMemberResult(entry)) { - if (!entry.query?.some((q) => q.includes(lcQuery))) return; // bail, does not match query + // do-not-bail, homeserver might be matching search query on attributes the client does not know about } else if (isPublicRoomResult(entry)) { if (!entry.query?.some((q) => q.includes(lcQuery))) return; // bail, does not match query } else { diff --git a/src/hooks/spotlight/useWebSearchMetrics.ts b/src/hooks/spotlight/useWebSearchMetrics.ts new file mode 100644 index 00000000000..b3675040fed --- /dev/null +++ b/src/hooks/spotlight/useWebSearchMetrics.ts @@ -0,0 +1,38 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useEffect } from "react"; +import { PosthogAnalytics } from "../../../src/PosthogAnalytics"; + +export const useWebSearchMetrics = (numResults: number, queryLength: number, viaSpotlight: boolean): void => { + useEffect(() => { + if (!queryLength) return; + + // send metrics after a 1s debounce + const timeoutId = window.setTimeout(() => { + PosthogAnalytics.instance.trackEvent({ + eventName: "WebSearch", + viaSpotlight, + numResults, + queryLength, + }); + }, 1000); + + return () => { + clearTimeout(timeoutId); + }; + }, [numResults, queryLength, viaSpotlight]); +}; diff --git a/test/components/views/dialogs/SpotlightDialog-test.tsx b/test/components/views/dialogs/SpotlightDialog-test.tsx index e587ffbe5cd..3822e6299e8 100644 --- a/test/components/views/dialogs/SpotlightDialog-test.tsx +++ b/test/components/views/dialogs/SpotlightDialog-test.tsx @@ -449,4 +449,59 @@ describe("Spotlight Dialog", () => { expect(screen.getByText(potatoRoom.name!)).toBeInTheDocument(); }); }); + + describe("user directory non-matrix attributes", () => { + interface IUserExternalAttributesChunkMember extends IUserChunkMember { + location?: string; + } + + const testPersonExternalAttributtes: IUserExternalAttributesChunkMember = { + user_id: "@earthling:matrix.org", + display_name: "Earth Person", + avatar_url: undefined, + location: "Earth", // this attribute could come from a matrix homeserver connected to LDAP + }; + + let mockedClient: MatrixClient; + beforeEach(() => { + const users: IUserExternalAttributesChunkMember[] = [testPersonExternalAttributtes] + mockedClient = mockClient({ rooms: [], users }); + /* overwrite the function searchUserDirectory, to include non-matrix search attributes, used by the homeserver */ + mockedClient.searchUserDirectory = jest.fn(({ term, limit }) => { + const searchTerm = term?.toLowerCase(); + const results = users.filter( + (it) => { + return ( + !searchTerm || + it.user_id.toLowerCase().includes(searchTerm) || + it.display_name?.toLowerCase().includes(searchTerm) || + it.location?.toLowerCase().includes(searchTerm) + ) + } + ); + return Promise.resolve({ + results: results.slice(0, limit ?? +Infinity), + limited: !!limit && limit < results.length, + }); + }); + }); + + it("should display in results, persons with non-matrix attributes matching the search query", async () => { + render( + null} + /> + ); + + // search is debounced + jest.advanceTimersByTime(200); + await flushPromisesWithFakeTimers(); + + const options = document.querySelectorAll("div.mx_SpotlightDialog_option"); + expect(options.length).toBeGreaterThanOrEqual(1); + expect(options[0]!.innerHTML).toContain(testPersonExternalAttributtes.display_name); + }); + }); });