Files
cinny/src/app/features/message-search/useMessageSearch.ts
T
jared dd2123da4b feat: encrypted room search via local cache scan
No Matrix web client supports E2EE message search server-side — the
homeserver only sees ciphertext. This is the same approach FluffyChat
takes: scan locally decrypted events already in the live timeline.

Changes:
- useLocalMessageSearch: searches getLiveTimeline().getEvents() in
  encrypted rooms using decrypted content (getContent(), not event.content)
- MessageSearch: runs client-side search in parallel with server search,
  shows results in a dedicated 'Encrypted Rooms' section with clear notice
  about scope (only cached/recently viewed messages)
- Encryption notice shown when encrypted rooms are in scope — explains
  why results may be missing and what to do
- Server result limit raised from 20 → 50

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:01:21 -04:00

116 lines
2.6 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[];
};
export const useMessageSearch = (params: MessageSearchParams) => {
const mx = useMatrixClient();
const { term, order, rooms, senders } = 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,
},
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;
};