feat(search): has:image/file/video filters + recent searches
- Add three msgtype toggle chips (Images/Files/Video) to the search filter bar, mirroring the existing "Has link" chip. The Matrix search API can't filter by msgtype server-side, so results are post-filtered client-side (union match on event.content.msgtype, dropping now-empty groups); the server request is unchanged. Visible count may be lower than the server total — inherent to client-side filtering. - Recent searches: last 10 distinct terms persisted via a new state/recentSearches.ts (atomWithStorage, error-safe, mirrors scheduledMessages). Shown as clickable chips when the search input is focused + empty, with a Clear affordance; clicking re-runs the search. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Text, Box, Icon, Icons, config, Spinner, IconButton, Line, toRem, Button } from 'folds';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
@@ -18,8 +18,14 @@ import { decodeSearchParamValueArray, encodeSearchParamValueArray } from '../../
|
||||
import { useRooms } from '../../state/hooks/roomList';
|
||||
import { allRoomsAtom } from '../../state/room-list/roomList';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { MessageSearchParams, useMessageSearch } from './useMessageSearch';
|
||||
import {
|
||||
filterGroupsByMsgType,
|
||||
MessageSearchParams,
|
||||
MsgTypeFilter,
|
||||
useMessageSearch,
|
||||
} from './useMessageSearch';
|
||||
import { useLocalMessageSearch } from './useLocalMessageSearch';
|
||||
import { addRecentSearch, recentSearchesAtom } from '../../state/recentSearches';
|
||||
import { SearchResultGroup } from './SearchResultGroup';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { SearchFilters } from './SearchFilters';
|
||||
@@ -167,6 +173,12 @@ export function MessageSearch({
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null) as React.RefObject<HTMLInputElement>;
|
||||
const scrollTopAnchorRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
||||
|
||||
// Client-side msgtype post-filter. Kept local — the Matrix search API cannot
|
||||
// filter by msgtype server-side, so the server request is unaffected.
|
||||
const [msgTypeFilters, setMsgTypeFilters] = useState<MsgTypeFilter[]>([]);
|
||||
const [recentSearches, setRecentSearches] = useAtom(recentSearchesAtom);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const searchPathSearchParams = useSearchPathSearchParams(searchParams);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
@@ -257,7 +269,10 @@ export function MessageSearch({
|
||||
getNextPageParam: (lastPage) => lastPage.nextToken,
|
||||
});
|
||||
|
||||
const groups = useMemo(() => data?.pages.flatMap((result) => result.groups) ?? [], [data]);
|
||||
const groups = useMemo(() => {
|
||||
const allGroups = data?.pages.flatMap((result) => result.groups) ?? [];
|
||||
return filterGroupsByMsgType(allGroups, msgTypeFilters);
|
||||
}, [data, msgTypeFilters]);
|
||||
const highlights = useMemo(() => {
|
||||
const mixed = data?.pages.flatMap((result) => result.highlights);
|
||||
return Array.from(new Set(mixed));
|
||||
@@ -278,7 +293,25 @@ export function MessageSearch({
|
||||
newParams.append('term', term);
|
||||
return newParams;
|
||||
});
|
||||
setRecentSearches((prev) => addRecentSearch(prev, term));
|
||||
};
|
||||
|
||||
const handleRecentSearch = (term: string) => {
|
||||
if (searchInputRef.current) {
|
||||
searchInputRef.current.value = term;
|
||||
}
|
||||
handleSearch(term);
|
||||
};
|
||||
|
||||
const handleToggleMsgTypeFilter = useCallback((msgType: MsgTypeFilter) => {
|
||||
setMsgTypeFilters((prev) =>
|
||||
prev.includes(msgType) ? prev.filter((t) => t !== msgType) : [...prev, msgType],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleClearRecentSearches = useCallback(() => {
|
||||
setRecentSearches([]);
|
||||
}, [setRecentSearches]);
|
||||
const handleSearchClear = () => {
|
||||
if (searchInputRef.current) {
|
||||
searchInputRef.current.value = '';
|
||||
@@ -407,6 +440,9 @@ export function MessageSearch({
|
||||
onSearch={handleSearch}
|
||||
onReset={handleSearchClear}
|
||||
onSenderAdd={handleSenderAdd}
|
||||
recentSearches={recentSearches}
|
||||
onRecentSearch={handleRecentSearch}
|
||||
onClearRecentSearches={handleClearRecentSearches}
|
||||
/>
|
||||
<SearchFilters
|
||||
defaultRoomsFilterName={defaultRoomsFilterName}
|
||||
@@ -425,6 +461,8 @@ export function MessageSearch({
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
containsUrl={msgSearchParams.containsUrl}
|
||||
onContainsUrlChange={handleContainsUrlChange}
|
||||
msgTypeFilters={msgTypeFilters}
|
||||
onToggleMsgTypeFilter={handleToggleMsgTypeFilter}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user