feat: sender filter for message search with from:@user autocomplete
CI / Build & Quality Checks (push) Successful in 10m17s
CI / Build & Quality Checks (push) Successful in 10m17s
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:
@@ -214,9 +214,19 @@ export function MessageSearch({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const localResult = useMemo(() => {
|
const localResult = useMemo(() => {
|
||||||
if (!msgSearchParams.term) return null;
|
if (!msgSearchParams.term) return null;
|
||||||
return searchLocalMessages(localSearchRooms, msgSearchParams.term);
|
return searchLocalMessages({
|
||||||
|
term: msgSearchParams.term,
|
||||||
|
roomIds: localSearchRooms,
|
||||||
|
senders: msgSearchParams.senders,
|
||||||
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [searchLocalMessages, localSearchRooms, msgSearchParams.term, cacheVersion]);
|
}, [
|
||||||
|
searchLocalMessages,
|
||||||
|
localSearchRooms,
|
||||||
|
msgSearchParams.term,
|
||||||
|
msgSearchParams.senders,
|
||||||
|
cacheVersion,
|
||||||
|
]);
|
||||||
|
|
||||||
const { status, data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
const { status, data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
||||||
enabled: !!msgSearchParams.term,
|
enabled: !!msgSearchParams.term,
|
||||||
@@ -297,6 +307,29 @@ export function MessageSearch({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectedSendersChange = useCallback(
|
||||||
|
(newSenders?: string[]) => {
|
||||||
|
setSearchParams((prevParams) => {
|
||||||
|
const p = new URLSearchParams(prevParams);
|
||||||
|
p.delete('senders');
|
||||||
|
if (newSenders && newSenders.length > 0) {
|
||||||
|
p.append('senders', encodeSearchParamValueArray(newSenders));
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSearchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSenderAdd = useCallback(
|
||||||
|
(userId: string) => {
|
||||||
|
const current = searchParamsSenders ?? [];
|
||||||
|
if (current.includes(userId)) return;
|
||||||
|
handleSelectedSendersChange([...current, userId]);
|
||||||
|
},
|
||||||
|
[searchParamsSenders, handleSelectedSendersChange],
|
||||||
|
);
|
||||||
|
|
||||||
const lastVItem = vItems[vItems.length - 1];
|
const lastVItem = vItems[vItems.length - 1];
|
||||||
const lastVItemIndex: number | undefined = lastVItem?.index;
|
const lastVItemIndex: number | undefined = lastVItem?.index;
|
||||||
const lastGroupIndex = groups.length - 1;
|
const lastGroupIndex = groups.length - 1;
|
||||||
@@ -332,6 +365,7 @@ export function MessageSearch({
|
|||||||
searchInputRef={searchInputRef}
|
searchInputRef={searchInputRef}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onReset={handleSearchClear}
|
onReset={handleSearchClear}
|
||||||
|
onSenderAdd={handleSenderAdd}
|
||||||
/>
|
/>
|
||||||
<SearchFilters
|
<SearchFilters
|
||||||
defaultRoomsFilterName={defaultRoomsFilterName}
|
defaultRoomsFilterName={defaultRoomsFilterName}
|
||||||
@@ -343,6 +377,8 @@ export function MessageSearch({
|
|||||||
onGlobalChange={handleGlobalChange}
|
onGlobalChange={handleGlobalChange}
|
||||||
order={msgSearchParams.order}
|
order={msgSearchParams.order}
|
||||||
onOrderChange={handleOrderChange}
|
onOrderChange={handleOrderChange}
|
||||||
|
selectedSenders={searchParamsSenders}
|
||||||
|
onSelectedSendersChange={handleSelectedSendersChange}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { SearchOrderBy } from 'matrix-js-sdk';
|
|||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||||
import { getRoomIconSrc } from '../../utils/room';
|
import { getRoomIconSrc } from '../../utils/room';
|
||||||
import { factoryRoomIdByAtoZ } from '../../utils/sort';
|
import { factoryRoomIdByAtoZ } from '../../utils/sort';
|
||||||
import {
|
import {
|
||||||
@@ -335,6 +336,8 @@ type SearchFiltersProps = {
|
|||||||
onGlobalChange: (global?: boolean) => void;
|
onGlobalChange: (global?: boolean) => void;
|
||||||
order?: string;
|
order?: string;
|
||||||
onOrderChange: (order?: string) => void;
|
onOrderChange: (order?: string) => void;
|
||||||
|
selectedSenders?: string[];
|
||||||
|
onSelectedSendersChange: (senders?: string[]) => void;
|
||||||
};
|
};
|
||||||
export function SearchFilters({
|
export function SearchFilters({
|
||||||
defaultRoomsFilterName,
|
defaultRoomsFilterName,
|
||||||
@@ -346,6 +349,8 @@ export function SearchFilters({
|
|||||||
order,
|
order,
|
||||||
onGlobalChange,
|
onGlobalChange,
|
||||||
onOrderChange,
|
onOrderChange,
|
||||||
|
selectedSenders,
|
||||||
|
onSelectedSendersChange,
|
||||||
}: SearchFiltersProps) {
|
}: SearchFiltersProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
@@ -403,6 +408,30 @@ export function SearchFilters({
|
|||||||
selectedRooms={selectedRooms}
|
selectedRooms={selectedRooms}
|
||||||
onChange={onSelectedRoomsChange}
|
onChange={onSelectedRoomsChange}
|
||||||
/>
|
/>
|
||||||
|
{selectedSenders && selectedSenders.length > 0 && (
|
||||||
|
<Line
|
||||||
|
style={{ margin: `${config.space.S100} 0` }}
|
||||||
|
direction="Vertical"
|
||||||
|
variant="Surface"
|
||||||
|
size="300"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedSenders?.map((userId) => {
|
||||||
|
const user = mx.getUser(userId);
|
||||||
|
const name = user?.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
key={userId}
|
||||||
|
variant="Primary"
|
||||||
|
radii="Pill"
|
||||||
|
before={<Icon size="50" src={Icons.User} />}
|
||||||
|
after={<Icon size="50" src={Icons.Cross} />}
|
||||||
|
onClick={() => onSelectedSendersChange(selectedSenders.filter((id) => id !== userId))}
|
||||||
|
>
|
||||||
|
<Text size="T200">{name}</Text>
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<Box grow="Yes" data-spacing-node />
|
<Box grow="Yes" data-spacing-node />
|
||||||
<OrderButton order={order} onChange={onOrderChange} />
|
<OrderButton order={order} onChange={onOrderChange} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,67 +1,249 @@
|
|||||||
import React, { FormEventHandler, RefObject } from 'react';
|
import React, {
|
||||||
import { Box, Text, Input, Icon, Icons, Spinner, Chip, config } from 'folds';
|
ChangeEventHandler,
|
||||||
|
FormEventHandler,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Chip,
|
||||||
|
config,
|
||||||
|
Icon,
|
||||||
|
Icons,
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
PopOut,
|
||||||
|
RectCords,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
} from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
|
||||||
|
import { UserAvatar } from '../../components/user-avatar';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
|
|
||||||
type SearchProps = {
|
// Matches "from:@anything" anywhere in the query (no trailing space required)
|
||||||
|
const FROM_REGEX = /from:@([^\s]*)/gi;
|
||||||
|
// For detecting active typing: matches the trailing "from:@..." at cursor end
|
||||||
|
const FROM_TYPING_REGEX = /from:@([^\s]*)$/i;
|
||||||
|
|
||||||
|
type SearchInputProps = {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
searchInputRef: RefObject<HTMLInputElement>;
|
searchInputRef: RefObject<HTMLInputElement>;
|
||||||
onSearch: (term: string) => void;
|
onSearch: (term: string) => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
onSenderAdd?: (userId: string) => void;
|
||||||
};
|
};
|
||||||
export function SearchInput({ active, loading, searchInputRef, onSearch, onReset }: SearchProps) {
|
export function SearchInput({
|
||||||
|
active,
|
||||||
|
loading,
|
||||||
|
searchInputRef,
|
||||||
|
onSearch,
|
||||||
|
onReset,
|
||||||
|
onSenderAdd,
|
||||||
|
}: SearchInputProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
const inputWrapRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [fromQuery, setFromQuery] = useState('');
|
||||||
|
const [autocompleteAnchor, setAutocompleteAnchor] = useState<RectCords>();
|
||||||
|
|
||||||
|
const allUsers = useMemo(() => mx.getUsers(), [mx]);
|
||||||
|
|
||||||
|
const suggestedUsers = useMemo(() => {
|
||||||
|
const q = fromQuery.toLowerCase();
|
||||||
|
return allUsers
|
||||||
|
.filter(
|
||||||
|
(u) =>
|
||||||
|
u.userId !== mx.getUserId() &&
|
||||||
|
(u.userId.toLowerCase().includes(q) || (u.displayName ?? '').toLowerCase().includes(q)),
|
||||||
|
)
|
||||||
|
.slice(0, 8);
|
||||||
|
}, [allUsers, fromQuery, mx]);
|
||||||
|
|
||||||
|
const closeAutocomplete = useCallback(() => {
|
||||||
|
setFromQuery('');
|
||||||
|
setAutocompleteAnchor(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUserSelect = useCallback(
|
||||||
|
(userId: string) => {
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
searchInputRef.current.value = searchInputRef.current.value
|
||||||
|
.replace(FROM_TYPING_REGEX, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
closeAutocomplete();
|
||||||
|
onSenderAdd?.(userId);
|
||||||
|
// Return focus to input so user can keep typing
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
},
|
||||||
|
[searchInputRef, onSenderAdd, closeAutocomplete],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||||
|
const value = evt.target.value;
|
||||||
|
const match = FROM_TYPING_REGEX.exec(value);
|
||||||
|
if (match) {
|
||||||
|
setFromQuery(match[1]);
|
||||||
|
const rect = inputWrapRef.current?.getBoundingClientRect();
|
||||||
|
if (rect) setAutocompleteAnchor(rect);
|
||||||
|
} else {
|
||||||
|
closeAutocomplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSearchSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
const handleSearchSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { searchInput } = evt.target as HTMLFormElement & {
|
const { searchInput } = evt.target as HTMLFormElement & {
|
||||||
searchInput: HTMLInputElement;
|
searchInput: HTMLInputElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchTerm = searchInput.value.trim() || undefined;
|
const rawValue = searchInput.value.trim();
|
||||||
if (searchTerm) {
|
|
||||||
onSearch(searchTerm);
|
// Extract any from:@userId fragments and turn them into sender filters
|
||||||
|
const fromMatches = [...rawValue.matchAll(FROM_REGEX)];
|
||||||
|
fromMatches.forEach((match) => {
|
||||||
|
const raw = match[1];
|
||||||
|
const userId = raw.startsWith('@') ? raw : `@${raw}`;
|
||||||
|
onSenderAdd?.(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchTerm = rawValue.replace(FROM_REGEX, '').replace(/\s+/g, ' ').trim() || undefined;
|
||||||
|
|
||||||
|
// Clean up the from: fragments from the visible input
|
||||||
|
if (fromMatches.length > 0 && searchInputRef.current) {
|
||||||
|
searchInputRef.current.value = searchTerm ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchTerm) onSearch(searchTerm);
|
||||||
|
closeAutocomplete();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="form" direction="Column" gap="100" onSubmit={handleSearchSubmit}>
|
<Box as="form" direction="Column" gap="100" onSubmit={handleSearchSubmit}>
|
||||||
<span data-spacing-node />
|
<span data-spacing-node />
|
||||||
<Text size="L400">Search</Text>
|
<Text size="L400">Search</Text>
|
||||||
<Input
|
<PopOut
|
||||||
ref={searchInputRef}
|
anchor={autocompleteAnchor}
|
||||||
style={{ paddingRight: config.space.S300 }}
|
position="Bottom"
|
||||||
name="searchInput"
|
align="Start"
|
||||||
autoFocus
|
offset={4}
|
||||||
size="500"
|
content={
|
||||||
variant="Background"
|
suggestedUsers.length > 0 ? (
|
||||||
placeholder="Search for keyword"
|
<FocusTrap
|
||||||
autoComplete="off"
|
focusTrapOptions={{
|
||||||
before={
|
initialFocus: false,
|
||||||
active && loading ? (
|
onDeactivate: closeAutocomplete,
|
||||||
<Spinner variant="Secondary" size="200" />
|
clickOutsideDeactivates: true,
|
||||||
) : (
|
allowOutsideClick: true,
|
||||||
<Icon size="200" src={Icons.Search} />
|
escapeDeactivates: stopPropagation,
|
||||||
)
|
}}
|
||||||
}
|
|
||||||
after={
|
|
||||||
active ? (
|
|
||||||
<Chip
|
|
||||||
key="resetButton"
|
|
||||||
type="reset"
|
|
||||||
variant="Secondary"
|
|
||||||
size="400"
|
|
||||||
radii="Pill"
|
|
||||||
outlined
|
|
||||||
after={<Icon size="50" src={Icons.Cross} />}
|
|
||||||
onClick={onReset}
|
|
||||||
>
|
>
|
||||||
<Text size="B300">Clear</Text>
|
<Menu variant="Surface" style={{ minWidth: '240px', maxWidth: '320px' }}>
|
||||||
</Chip>
|
<Box direction="Column" style={{ padding: `${config.space.S100} 0` }}>
|
||||||
) : (
|
{suggestedUsers.map((user) => {
|
||||||
<Chip type="submit" variant="Primary" size="400" radii="Pill" outlined>
|
const displayName = user.displayName ?? getMxIdLocalPart(user.userId);
|
||||||
<Text size="B300">Enter</Text>
|
const avatarUrl = user.avatarUrl
|
||||||
</Chip>
|
? (mxcUrlToHttp(mx, user.avatarUrl, useAuthentication, 32, 32, 'crop') ??
|
||||||
)
|
undefined)
|
||||||
|
: undefined;
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={user.userId}
|
||||||
|
variant="Surface"
|
||||||
|
size="300"
|
||||||
|
radii="0"
|
||||||
|
// Prevent blur on the search input when clicking
|
||||||
|
onMouseDown={(e: React.MouseEvent) => e.preventDefault()}
|
||||||
|
onClick={() => handleUserSelect(user.userId)}
|
||||||
|
before={
|
||||||
|
<Avatar size="200" radii="Pill">
|
||||||
|
<UserAvatar
|
||||||
|
userId={user.userId}
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={displayName ?? user.userId}
|
||||||
|
renderFallback={() => <Icon size="100" src={Icons.User} filled />}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box direction="Column" style={{ minWidth: 0 }}>
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="T200"
|
||||||
|
truncate
|
||||||
|
style={{
|
||||||
|
opacity: 0.6,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.75em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user.userId}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<Box ref={inputWrapRef}>
|
||||||
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
|
style={{ paddingRight: config.space.S300 }}
|
||||||
|
name="searchInput"
|
||||||
|
autoFocus
|
||||||
|
size="500"
|
||||||
|
variant="Background"
|
||||||
|
placeholder="Search messages or type from:@user to filter by sender"
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={handleChange}
|
||||||
|
before={
|
||||||
|
active && loading ? (
|
||||||
|
<Spinner variant="Secondary" size="200" />
|
||||||
|
) : (
|
||||||
|
<Icon size="200" src={Icons.Search} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
after={
|
||||||
|
active ? (
|
||||||
|
<Chip
|
||||||
|
key="resetButton"
|
||||||
|
type="reset"
|
||||||
|
variant="Secondary"
|
||||||
|
size="400"
|
||||||
|
radii="Pill"
|
||||||
|
outlined
|
||||||
|
after={<Icon size="50" src={Icons.Cross} />}
|
||||||
|
onClick={onReset}
|
||||||
|
>
|
||||||
|
<Text size="B300">Clear</Text>
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<Chip type="submit" variant="Primary" size="400" radii="Pill" outlined>
|
||||||
|
<Text size="B300">Enter</Text>
|
||||||
|
</Chip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</PopOut>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { useCallback } from 'react';
|
|||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { ResultGroup, ResultItem } from './useMessageSearch';
|
import { ResultGroup, ResultItem } from './useMessageSearch';
|
||||||
|
|
||||||
|
export type LocalSearchParams = {
|
||||||
|
term: string;
|
||||||
|
roomIds: string[];
|
||||||
|
senders?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type LocalSearchResult = {
|
export type LocalSearchResult = {
|
||||||
groups: ResultGroup[];
|
groups: ResultGroup[];
|
||||||
/** How many rooms in scope are encrypted */
|
/** How many rooms in scope are encrypted */
|
||||||
@@ -23,13 +29,14 @@ export const useLocalMessageSearch = () => {
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
const search = useCallback(
|
const search = useCallback(
|
||||||
(roomIds: string[], term: string): LocalSearchResult => {
|
({ term, roomIds, senders }: LocalSearchParams): LocalSearchResult => {
|
||||||
const trimmedTerm = term.trim();
|
const trimmedTerm = term.trim();
|
||||||
if (!trimmedTerm) {
|
if (!trimmedTerm) {
|
||||||
return { groups: [], encryptedRoomsCount: 0, searchedRoomsCount: 0 };
|
return { groups: [], encryptedRoomsCount: 0, searchedRoomsCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const termLower = trimmedTerm.toLowerCase();
|
const termLower = trimmedTerm.toLowerCase();
|
||||||
|
const senderSet = senders && senders.length > 0 ? new Set(senders) : null;
|
||||||
const groups: ResultGroup[] = [];
|
const groups: ResultGroup[] = [];
|
||||||
let encryptedRoomsCount = 0;
|
let encryptedRoomsCount = 0;
|
||||||
let searchedRoomsCount = 0;
|
let searchedRoomsCount = 0;
|
||||||
@@ -56,6 +63,7 @@ export const useLocalMessageSearch = () => {
|
|||||||
if (event.getType() !== EventType.RoomMessage) continue;
|
if (event.getType() !== EventType.RoomMessage) continue;
|
||||||
if (event.isDecryptionFailure()) continue;
|
if (event.isDecryptionFailure()) continue;
|
||||||
if (event.isRedacted()) continue;
|
if (event.isRedacted()) continue;
|
||||||
|
if (senderSet && !senderSet.has(event.getSender() ?? '')) continue;
|
||||||
|
|
||||||
// getContent() returns decrypted plaintext regardless of encryption
|
// getContent() returns decrypted plaintext regardless of encryption
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
|
|||||||
Reference in New Issue
Block a user