feat: sender filter for message search with from:@user autocomplete

Type 'from:@name' in the search box to filter by sender — a dropdown
of matching users (avatar + display name + full ID) appears as you type
and selecting one converts it into a removable sender chip in the filter
bar. Multiple senders supported. Also works via manual entry on submit.

- SearchInput: detects trailing 'from:@...' pattern on every keystroke,
  shows PopOut autocomplete from mx.getUsers(), onMouseDown prevents
  input blur when selecting, cleans up fragment after selection
- SearchFilters: selectedSenders/onSelectedSendersChange props, sender
  chips rendered with user icon and X to remove
- useLocalMessageSearch: filters cached events by sender set when senders
  param is provided (encrypted room search respects the filter too)
- MessageSearch: handleSenderAdd deduplicates and writes to ?senders= URL
  param; localResult now passes senders to the local search

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 22:07:53 -04:00
parent e5ae77a99d
commit af3155e169
4 changed files with 300 additions and 45 deletions
@@ -3,6 +3,12 @@ import { useCallback } from 'react';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { ResultGroup, ResultItem } from './useMessageSearch';
export type LocalSearchParams = {
term: string;
roomIds: string[];
senders?: string[];
};
export type LocalSearchResult = {
groups: ResultGroup[];
/** How many rooms in scope are encrypted */
@@ -23,13 +29,14 @@ export const useLocalMessageSearch = () => {
const mx = useMatrixClient();
const search = useCallback(
(roomIds: string[], term: string): LocalSearchResult => {
({ term, roomIds, senders }: LocalSearchParams): LocalSearchResult => {
const trimmedTerm = term.trim();
if (!trimmedTerm) {
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;
@@ -56,6 +63,7 @@ export const useLocalMessageSearch = () => {
if (event.getType() !== EventType.RoomMessage) continue;
if (event.isDecryptionFailure()) continue;
if (event.isRedacted()) continue;
if (senderSet && !senderSet.has(event.getSender() ?? '')) continue;
// getContent() returns decrypted plaintext regardless of encryption
const content = event.getContent();