Files
cinny/src/app/features/message-search/useMessageSearch.ts
T
jared dfedba9ef8
CI / Build & Quality Checks (push) Successful in 10m30s
feat: document title unread count, draft persistence, search date range
E1 - Document title unread count: FaviconUpdater now also sets
     document.title to '(N) Lotus Chat' for mentions, '· Lotus Chat'
     for plain unreads, and 'Lotus Chat' when clear. Reuses the
     existing roomToUnread forEach loop.

E2 - Draft persistence across reloads: on room unmount, unsent message
     is written to localStorage as 'draft-msg-<roomId>'. On mount, if
     the Jotai atom is empty (page reload), the localStorage draft is
     restored. Cleared on send. Uses the existing Slate node JSON format.

E5 - Search date range filter: new DateRangeButton in SearchFilters
     with From/To date inputs in a PopOut. Dates stored as epoch ms in
     ?fromTs=&toTs= URL params. Passed to Matrix /search as from_ts /
     to_ts filter fields (valid spec fields, cast via 'as any' since
     SDK types don't include them yet).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 22:22:40 -04:00

122 lines
2.9 KiB
TypeScript

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[];
};
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;
};
export const useMessageSearch = (params: MessageSearchParams) => {
const mx = useMatrixClient();
const { term, order, rooms, senders, fromTs, toTs } = 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,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
filter: {
limit,
rooms,
senders,
// from_ts / to_ts are valid Matrix spec fields not yet in SDK types
...(fromTs !== undefined && { from_ts: fromTs }),
...(toTs !== undefined && { to_ts: toTs }),
} 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],
);
return searchMessages;
};