feat(search): support from:username with no body text
Typing "from:jared" with no additional text now shows all cached messages from that user across all rooms. - SearchInput: call onSearch() even when only from: tokens were extracted (no remaining body text), passing an empty string term - useLocalMessageSearch: introduce senderOnlyMode (empty term + senders set) which searches ALL rooms instead of encrypted-only, and skips text matching — just filters by sender - MessageSearch: define hasActiveSearch / senderOnlyMode flags; use them to enable local search and fix placeholder/loading/results conditions; adapt local results section header and description Server-side search is skipped in sender-only mode (Matrix search API requires a search_term); results come from the local event cache. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,12 +31,16 @@ export const useLocalMessageSearch = () => {
|
||||
const search = useCallback(
|
||||
({ term, roomIds, senders }: LocalSearchParams): LocalSearchResult => {
|
||||
const trimmedTerm = term.trim();
|
||||
if (!trimmedTerm) {
|
||||
const senderSet = senders && senders.length > 0 ? new Set(senders) : null;
|
||||
|
||||
// Sender-only mode: no text filter, search all rooms (server can't filter by sender alone)
|
||||
const senderOnlyMode = !trimmedTerm && !!senderSet;
|
||||
|
||||
if (!trimmedTerm && !senderSet) {
|
||||
return { groups: [], encryptedRoomsCount: 0, searchedRoomsCount: 0 };
|
||||
}
|
||||
|
||||
const termLower = trimmedTerm.toLowerCase();
|
||||
const senderSet = senders && senders.length > 0 ? new Set(senders) : null;
|
||||
const groups: ResultGroup[] = [];
|
||||
let encryptedRoomsCount = 0;
|
||||
let searchedRoomsCount = 0;
|
||||
@@ -46,9 +50,12 @@ export const useLocalMessageSearch = () => {
|
||||
if (!room) continue;
|
||||
|
||||
const isEncrypted = !!room.currentState.getStateEvents(EventType.RoomEncryption, '');
|
||||
if (!isEncrypted) continue;
|
||||
|
||||
encryptedRoomsCount += 1;
|
||||
// Text search: encrypted rooms only — server already covers plaintext rooms
|
||||
// Sender-only: all rooms — server has no sender-only search
|
||||
if (!senderOnlyMode && !isEncrypted) continue;
|
||||
|
||||
if (isEncrypted) encryptedRoomsCount += 1;
|
||||
|
||||
const events = room
|
||||
.getUnfilteredTimelineSet()
|
||||
@@ -63,13 +70,16 @@ export const useLocalMessageSearch = () => {
|
||||
for (let i = 0; i < events.length; i += 1) {
|
||||
const event = events[i];
|
||||
|
||||
const evType = event.getType();
|
||||
const isMessage = evType === EventType.RoomMessage;
|
||||
const isSticker = evType === 'm.sticker';
|
||||
const isPoll =
|
||||
evType === 'm.poll.start' || evType === 'org.matrix.msc3381.poll.start';
|
||||
// In sender-only mode: include all message types; skip non-message events
|
||||
if (event.getType() !== EventType.RoomMessage) {
|
||||
if (senderOnlyMode) continue;
|
||||
const evType = event.getType();
|
||||
const isSticker = evType === 'm.sticker';
|
||||
const isPoll =
|
||||
evType === 'm.poll.start' || evType === 'org.matrix.msc3381.poll.start';
|
||||
if (!isSticker && !isPoll) continue;
|
||||
}
|
||||
|
||||
if (!isMessage && !isSticker && !isPoll) continue;
|
||||
if (event.isDecryptionFailure()) continue;
|
||||
if (event.isRedacted()) continue;
|
||||
if (senderSet && !senderSet.has(event.getSender() ?? '')) continue;
|
||||
@@ -77,41 +87,49 @@ export const useLocalMessageSearch = () => {
|
||||
// getContent() returns decrypted plaintext regardless of encryption
|
||||
const content = event.getContent();
|
||||
|
||||
let body = '';
|
||||
let formattedBody = '';
|
||||
if (isMessage || isSticker) {
|
||||
body = (content.body as string | undefined) ?? '';
|
||||
formattedBody = (content.formatted_body as string | undefined) ?? '';
|
||||
} else {
|
||||
// Poll — index question text and all answer options
|
||||
const poll = (content['m.poll'] ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content['org.matrix.msc3381.poll.start']) as any;
|
||||
if (poll) {
|
||||
const qBody =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(poll.question?.['m.text'] as Array<{ body: string }> | undefined)?.[0]?.body ??
|
||||
(poll.question?.body as string | undefined) ??
|
||||
'';
|
||||
const answerBodies = ((poll.answers ?? []) as Array<Record<string, unknown>>)
|
||||
.map(
|
||||
(a) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
((a['m.text'] as Array<{ body: string }> | undefined)?.[0]?.body ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(a['org.matrix.msc3381.poll.answer'] as any)?.body ??
|
||||
'') as string,
|
||||
)
|
||||
.join(' ');
|
||||
body = `${qBody} ${answerBodies}`.trim();
|
||||
}
|
||||
}
|
||||
// Sender-only mode: no text filter needed
|
||||
if (!senderOnlyMode) {
|
||||
const evType = event.getType();
|
||||
const isSticker = evType === 'm.sticker';
|
||||
const isPoll =
|
||||
evType === 'm.poll.start' || evType === 'org.matrix.msc3381.poll.start';
|
||||
|
||||
if (
|
||||
!body.toLowerCase().includes(termLower) &&
|
||||
!formattedBody.toLowerCase().includes(termLower)
|
||||
)
|
||||
continue;
|
||||
let body = '';
|
||||
let formattedBody = '';
|
||||
if (!isPoll) {
|
||||
body = (content.body as string | undefined) ?? '';
|
||||
formattedBody = (content.formatted_body as string | undefined) ?? '';
|
||||
} else {
|
||||
// Poll — index question text and all answer options
|
||||
const poll = (content['m.poll'] ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content['org.matrix.msc3381.poll.start']) as any;
|
||||
if (poll) {
|
||||
const qBody =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(poll.question?.['m.text'] as Array<{ body: string }> | undefined)?.[0]?.body ??
|
||||
(poll.question?.body as string | undefined) ??
|
||||
'';
|
||||
const answerBodies = ((poll.answers ?? []) as Array<Record<string, unknown>>)
|
||||
.map(
|
||||
(a) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
((a['m.text'] as Array<{ body: string }> | undefined)?.[0]?.body ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(a['org.matrix.msc3381.poll.answer'] as any)?.body ??
|
||||
'') as string,
|
||||
)
|
||||
.join(' ');
|
||||
body = `${qBody} ${answerBodies}`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!body.toLowerCase().includes(termLower) &&
|
||||
!formattedBody.toLowerCase().includes(termLower)
|
||||
)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build a synthetic IEventWithRoomId using decrypted content so the
|
||||
// existing SearchResultGroup renderer works without modification.
|
||||
|
||||
Reference in New Issue
Block a user