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); }; 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(); const { term, order, rooms, senders, fromTs, toTs, 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, // from_ts / to_ts and contains_url are valid Matrix spec fields not yet in SDK types ...(fromTs !== undefined && { from_ts: fromTs }), ...(toTs !== undefined && { to_ts: toTs }), ...(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, fromTs, toTs, containsUrl], ); return searchMessages; };