import { IEventWithRoomId, IResultContext, ISearchRequestBody, ISearchResponse, ISearchResult, SearchOrderBy, } from 'matrix-js-sdk'; import { useCallback } from 'react'; import { useMatrixClient } from '../../hooks/useMatrixClient'; export type ResultItem = { rank: number; event: IEventWithRoomId; context: IResultContext; }; export type ResultGroup = { roomId: string; items: ResultItem[]; }; export type SearchResult = { nextToken?: string; highlights: string[]; groups: ResultGroup[]; }; // Client-side msgtype post-filter. The Matrix search API cannot filter by // msgtype server-side, so this is applied to already-returned results. export type MsgTypeFilter = 'm.image' | 'm.file' | 'm.video'; /** * Filter result groups to items whose event msgtype is in `msgTypes` (OR/union). * Empty/absent filter returns groups unchanged. Now-empty groups are dropped. */ export const filterGroupsByMsgType = ( groups: ResultGroup[], msgTypes: MsgTypeFilter[], ): ResultGroup[] => { if (msgTypes.length === 0) return groups; const allowed = new Set(msgTypes); return groups .map((group) => ({ ...group, items: group.items.filter((item) => { const msgtype = item.event.content?.msgtype; return typeof msgtype === 'string' && allowed.has(msgtype); }), })) .filter((group) => group.items.length > 0); }; /** * Filter result groups to items whose event is currently pinned in its room. * `isPinned(roomId, eventId)` returns whether the event is in the room's * `m.room.pinned_events` set. When `enabled` is false, groups are returned * unchanged. Now-empty groups are dropped. */ export const filterGroupsByPinned = ( groups: ResultGroup[], enabled: boolean, isPinned: (roomId: string, eventId: string) => boolean, ): ResultGroup[] => { if (!enabled) return groups; return groups .map((group) => ({ ...group, items: group.items.filter((item) => isPinned(group.roomId, item.event.event_id)), })) .filter((group) => group.items.length > 0); }; /** Inclusive-range predicate, mirrored from `inRange` in useLocalMessageSearch.ts. */ export const inTsRange = (ts: number, fromTs?: number, toTs?: number): boolean => (fromTs === undefined || ts >= fromTs) && (toTs === undefined || ts <= toTs); /** * Filter result groups to items whose `origin_server_ts` falls within * [fromTs, toTs] (inclusive, either bound optional). The Matrix search API * has no timestamp filter fields, so server results must be post-filtered * here — the same predicate the local/encrypted search already applies. * Now-empty groups are dropped. */ export const filterGroupsByDateRange = ( groups: ResultGroup[], fromTs?: number, toTs?: number, ): ResultGroup[] => { if (fromTs === undefined && toTs === undefined) return groups; return groups .map((group) => ({ ...group, items: group.items.filter((item) => inTsRange(item.event.origin_server_ts, fromTs, toTs)), })) .filter((group) => group.items.length > 0); }; const groupSearchResult = (results: ISearchResult[]): ResultGroup[] => { const groups: ResultGroup[] = []; results.forEach((item) => { const roomId = item.result.room_id; const resultItem: ResultItem = { rank: item.rank, event: item.result, context: item.context, }; const lastAddedGroup: ResultGroup | undefined = groups[groups.length - 1]; if (lastAddedGroup && roomId === lastAddedGroup.roomId) { lastAddedGroup.items.push(resultItem); return; } groups.push({ roomId, items: [resultItem], }); }); return groups; }; const parseSearchResult = (result: ISearchResponse): SearchResult => { const roomEvents = result.search_categories.room_events; const searchResult: SearchResult = { nextToken: roomEvents?.next_batch, highlights: roomEvents?.highlights ?? [], groups: groupSearchResult(roomEvents?.results ?? []), }; return searchResult; }; export type MessageSearchParams = { term?: string; order?: string; rooms?: string[]; senders?: string[]; fromTs?: number; toTs?: number; containsUrl?: boolean; }; export const useMessageSearch = (params: MessageSearchParams) => { const mx = useMatrixClient(); // fromTs/toTs are intentionally not sent to the server (see comment below) — // callers post-filter results with filterGroupsByDateRange instead. const { term, order, rooms, senders, containsUrl } = params; const searchMessages = useCallback( async (nextBatch?: string) => { if (!term) return { highlights: [], groups: [], }; const limit = 50; const requestBody: ISearchRequestBody = { search_categories: { room_events: { event_context: { before_limit: 0, after_limit: 0, include_profile: false, }, filter: { limit, rooms, senders, // `RoomEventFilter` has no timestamp bounds — from_ts/to_ts are not // Matrix filter fields and the homeserver silently drops them, so the // date range is instead enforced client-side (see filterGroupsByDateRange). // contains_url is a valid spec field not yet in SDK types. ...(containsUrl !== undefined && { contains_url: containsUrl }), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, include_state: false, order_by: order as SearchOrderBy.Recent, search_term: term, }, }, }; const r = await mx.search({ body: requestBody, next_batch: nextBatch === '' ? undefined : nextBatch, }); return parseSearchResult(r); }, [mx, term, order, rooms, senders, containsUrl], ); return searchMessages; };