Search: Add sender filter (#6578)

This commit is contained in:
Alexander Zinchuk 2026-01-20 12:01:16 +01:00
parent 3eb69cefb1
commit 224670674a
13 changed files with 339 additions and 54 deletions

View File

@ -1476,6 +1476,7 @@ export async function searchMessagesInChat({
offsetId,
addOffset,
limit,
fromPeer,
}: {
peer: ApiPeer;
isSavedDialog?: boolean;
@ -1488,6 +1489,7 @@ export async function searchMessagesInChat({
limit: number;
minDate?: number;
maxDate?: number;
fromPeer?: ApiPeer;
}): Promise<SearchResults | undefined> {
let filter;
switch (type) {
@ -1519,6 +1521,7 @@ export async function searchMessagesInChat({
}
const inputPeer = buildInputPeer(peer.id, peer.accessHash);
const inputFromPeer = fromPeer ? buildInputPeer(fromPeer.id, fromPeer.accessHash) : undefined;
const result = await invokeRequest(new GramJs.messages.Search({
peer: isSavedDialog ? new GramJs.InputPeerSelf() : inputPeer,
@ -1527,6 +1530,7 @@ export async function searchMessagesInChat({
topMsgId: threadId !== MAIN_THREAD_ID && !isSavedDialog ? Number(threadId) : undefined,
filter,
q: query,
fromId: inputFromPeer,
minDate: minDate ?? DEFAULT_PRIMITIVES.INT,
maxDate: maxDate ?? DEFAULT_PRIMITIVES.INT,
maxId: DEFAULT_PRIMITIVES.INT,

View File

@ -1711,6 +1711,7 @@
"GroupChatsSearchContext" = "Group Chats";
"ChannelsSearchContext" = "Channels";
"SearchContextCaption" = "From {type}";
"SearchFilterFrom" = "From:";
"FolderLinkTitleDescription" = "Anyone with this link can add {folder} folder and {chats} selected below.";
"FolderLinkTitleDescriptionChats_one" = "the chat";
"FolderLinkTitleDescriptionChats_other" = "the {count} chats";

View File

@ -19,10 +19,12 @@ import {
getMainUsername,
isAnonymousForwardsChat,
isAnonymousOwnMessage,
isChatChannel,
isSystemBot,
} from '../../../global/helpers';
import { isApiPeerUser } from '../../../global/helpers/peers';
import {
selectChat,
selectForwardedSender,
selectIsChatWithSelf,
selectSender,
@ -58,6 +60,7 @@ type StateProps = {
isChatWithSelf?: boolean;
isRepliesChat?: boolean;
isAnonymousForwards?: boolean;
isChannel?: boolean;
};
const SenderGroupContainer: FC<OwnProps & StateProps> = ({
@ -72,9 +75,10 @@ const SenderGroupContainer: FC<OwnProps & StateProps> = ({
isChatWithSelf,
isRepliesChat,
isAnonymousForwards,
isChannel,
canPost,
}) => {
const { openChat, updateInsertingPeerIdMention } = getActions();
const { openChat, updateInsertingPeerIdMention, openMiddleSearch } = getActions();
const { forwardInfo } = message;
@ -115,6 +119,14 @@ const SenderGroupContainer: FC<OwnProps & StateProps> = ({
}
});
const handleSearchMessages = useLastCallback(() => {
if (!avatarPeer) {
return;
}
openMiddleSearch({ fromPeerId: avatarPeer.id });
});
const handleAvatarClick = useLastCallback(() => {
handleOpenChat();
});
@ -142,7 +154,8 @@ const SenderGroupContainer: FC<OwnProps & StateProps> = ({
const getLayout = useLastCallback(() => ({ withPortal: true }));
const canMention = canPost && avatarPeer && (isAvatarPeerUser || Boolean(getMainUsername(avatarPeer)));
const shouldRenderContextMenu = Boolean(contextMenuAnchor) && (isAvatarPeerUser || canMention);
const canSearch = !isChannel;
const shouldRenderContextMenu = Boolean(contextMenuAnchor) && (isAvatarPeerUser || canMention || canSearch);
function renderContextMenu() {
return (
@ -176,6 +189,14 @@ const SenderGroupContainer: FC<OwnProps & StateProps> = ({
{lang('ContextMenuItemMention')}
</MenuItem>
)}
{canSearch && (
<MenuItem
icon="search"
onClick={handleSearchMessages}
>
{lang('Search')}
</MenuItem>
)}
</>
</Menu>
);
@ -221,6 +242,7 @@ export default memo(withGlobal<OwnProps>(
} = ownProps;
const { chatId } = message;
const chat = selectChat(global, chatId);
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isSystemBotChat = isSystemBot(chatId);
const isAnonymousForwards = isAnonymousForwardsChat(chatId);
@ -237,6 +259,7 @@ export default memo(withGlobal<OwnProps>(
isChatWithSelf,
isRepliesChat: isSystemBotChat,
isAnonymousForwards,
isChannel: chat && isChatChannel(chat),
};
},
)(SenderGroupContainer));

View File

@ -202,6 +202,21 @@
color: var(--color-text-secondary);
}
.fromTag {
--color-chat-hover: var(--color-borders);
display: flex;
gap: 0.25rem;
align-items: center;
margin-inline-start: 0.5rem;
font-size: 1rem;
color: var(--color-text);
white-space: nowrap;
}
.searchTypes {
overflow-x: scroll;
display: flex;
@ -235,6 +250,12 @@
}
}
.icons {
display: flex;
gap: 0.25rem;
align-items: center;
}
.footer {
pointer-events: auto;
@ -306,3 +327,9 @@
transform: translateY(0);
}
}
.memberItem {
:global(.ListItem-button) {
padding: 0.375rem 0.75rem !important;
}
}

View File

@ -16,7 +16,7 @@ import type {
import { ANONYMOUS_USER_ID } from '../../../config';
import { requestMeasure, requestMutation, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
import {
getIsSavedDialog, getReactionKey, isSameReaction, isSystemBot,
getIsSavedDialog, getReactionKey, isChatGroup, isSameReaction, isSystemBot,
} from '../../../global/helpers';
import {
selectChat,
@ -27,6 +27,7 @@ import {
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectMonoforumChannel,
selectPeer,
selectSender,
selectTabState,
} from '../../../global/selectors';
@ -50,11 +51,13 @@ import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation'
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import usePeerSearch, { prepareChatMemberSearch } from '../../../hooks/usePeerSearch';
import Avatar from '../../common/Avatar';
import PeerChip from '../../common/PeerChip';
import Button from '../../ui/Button';
import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading';
import SearchInput from '../../ui/SearchInput';
import SavedTagButton from '../message/reactions/SavedTagButton';
import MiddleSearchResult from './MiddleSearchResult';
@ -82,6 +85,8 @@ type StateProps = {
isHashtagQuery?: boolean;
searchType?: MiddleSearchType;
currentUserId?: string;
fromPeerId?: string;
isGroupChat?: boolean;
};
const CHANNELS_PEER: CustomPeer = {
@ -92,6 +97,8 @@ const CHANNELS_PEER: CustomPeer = {
const FOCUSED_SEARCH_TRIGGER_OFFSET = 5;
const HIDE_TIMEOUT = 200;
const RESULT_ITEM_CLASS_NAME = 'MiddleSearchResult';
const FROM_FILTER_PREFIX = 'from:';
const INLINE_MEMBER_COUNT = 5;
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
@ -113,6 +120,8 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
isHashtagQuery,
searchType = 'chat',
currentUserId,
fromPeerId,
isGroupChat,
}) => {
const {
updateMiddleSearch,
@ -146,6 +155,28 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
const [isFocused, markFocused, markBlurred] = useFlag();
const [isViewAsList, setIsViewAsList] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isFromFilterMode, setIsFromFilterMode] = useState(Boolean(fromPeerId));
const [memberSearchQuery, setMemberSearchQuery] = useState('');
useEffect(() => {
if (fromPeerId) {
setIsFromFilterMode(true);
focusInput();
}
}, [fromPeerId]);
const memberSearchFn = useMemo(
() => (chat && isGroupChat ? prepareChatMemberSearch(chat) : undefined),
[chat, isGroupChat],
);
const hasMemberDropdown = isFromFilterMode && !fromPeerId;
const { result: memberSearchResults, isLoading: isMemberSearchLoading } = usePeerSearch({
query: hasMemberDropdown ? (memberSearchQuery || ' ') : query,
queryFn: memberSearchFn,
isDisabled: !isGroupChat || Boolean(fromPeerId) || (!isFromFilterMode && (!query || isHashtagQuery)),
});
const handleClickOutside = useLastCallback((event: MouseEvent) => {
if (maybeLongPressActiveRef.current) return;
@ -155,17 +186,11 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
});
useClickOutside([ref], handleClickOutside);
const hasResultsContainer = Boolean((query && foundIds) || isHashtagQuery);
const hasResultsContainer = Boolean(((query || fromPeerId) && foundIds) || isHashtagQuery);
const isOnlyHash = isHashtagQuery && !query;
const areResultsEmpty = Boolean(query && foundIds && !foundIds.length && !isLoading && !isOnlyHash);
const hasResultsPlaceholder = areResultsEmpty || isOnlyHash;
const isNonFocusedDropdownForced = searchType === 'myChats' || searchType === 'channels';
const hasResultsDropdown = isActive && (isViewAsList || !isMobile) && (isFocused || isNonFocusedDropdownForced)
&& Boolean(
hasResultsContainer || hasResultsPlaceholder || savedTags,
);
const hasQueryData = Boolean((query && !isOnlyHash) || savedTag);
const hasMemberResults = !isHashtagQuery && Boolean(memberSearchResults?.length);
const hasQueryData = Boolean((query && !isOnlyHash) || savedTag || fromPeerId);
const hasNavigationButtons = searchType === 'chat' && Boolean(foundIds?.length);
const handleClose = useLastCallback(() => {
@ -236,6 +261,8 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
setIsViewAsList(true);
setFocusedIndex(0);
setQuery('');
setIsFromFilterMode(false);
setMemberSearchQuery('');
hiddenTimerRef.current = window.setTimeout(() => setIsFullyHidden(true), HIDE_TIMEOUT);
} else {
setIsFullyHidden(false);
@ -274,13 +301,15 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
}, [isHistoryCalendarOpen, isActive]);
const handleReset = useLastCallback(() => {
if (!query?.length && !savedTag) {
if (!query?.length && !savedTag && !fromPeerId && !isFromFilterMode) {
handleClose();
return;
}
setQuery('');
setIsLoading(false);
setIsFromFilterMode(false);
setMemberSearchQuery('');
resetMiddleSearch();
focusInput();
});
@ -321,9 +350,23 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
return;
}
if (isGroupChat && newQuery.toLowerCase().startsWith(FROM_FILTER_PREFIX) && !fromPeerId && !isFromFilterMode) {
setIsFromFilterMode(true);
const memberQuery = newQuery.slice(FROM_FILTER_PREFIX.length).trim();
setMemberSearchQuery(memberQuery);
return;
}
// When in from filter mode (selecting a member), update member search query
if (isFromFilterMode && !fromPeerId) {
setMemberSearchQuery(newQuery);
return;
}
// Normal query update (including when fromPeerId is set - query is separate)
setQuery(newQuery);
if (!newQuery) {
if (!newQuery && !fromPeerId) {
setIsLoading(false);
resetMiddleSearch();
shouldCancelSearchRef.current = true;
@ -331,10 +374,10 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
});
useEffect(() => {
if (query) {
if (isActive && (query || fromPeerId)) {
handleSearch();
}
}, [query]);
}, [isActive, query, fromPeerId]);
useEffect(() => {
setIsLoading(Boolean(fetchingQuery));
@ -391,8 +434,16 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
const [viewportIds, getMore, viewportOffset = 0] = useInfiniteScroll(handleSearch, foundIds);
const [viewportMemberIds, getMoreMembers] = useInfiniteScroll(
undefined,
hasMemberDropdown ? memberSearchResults : undefined,
);
const displayedMembers = (hasMemberResults
&& (hasMemberDropdown ? viewportMemberIds : memberSearchResults.slice(0, INLINE_MEMBER_COUNT))) || undefined;
const viewportResults = useMemo(() => {
if ((!query && !savedTag) || !viewportIds?.length) {
if ((!query && !savedTag && !fromPeerId) || !viewportIds?.length) {
return MEMO_EMPTY_ARRAY;
}
const global = getGlobal();
@ -418,7 +469,16 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
senderPeer,
};
}).filter(Boolean);
}, [query, savedTag, viewportIds, isSavedMessages]);
}, [query, savedTag, fromPeerId, viewportIds, isSavedMessages]);
const areResultsEmpty = Boolean(
(query || fromPeerId) && !isLoading && !viewportResults.length && !isOnlyHash && !hasMemberResults,
);
const hasResultsPlaceholder = areResultsEmpty || isOnlyHash;
const hasResultsDropdown = isActive && (isViewAsList || !isMobile) && (isFocused || isNonFocusedDropdownForced)
&& Boolean(
hasResultsContainer || hasResultsPlaceholder || savedTags || hasMemberResults,
);
const handleMessageClick = useLastCallback((message: ApiMessage) => {
const searchResultKey = getSearchResultKey(message);
@ -444,13 +504,33 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
markFocused();
});
const handleKeyDown = useKeyboardListNavigation(containerRef, hasResultsContainer, (index) => {
const foundResult = viewportResults?.[index === -1 ? 0 : index];
if (foundResult) {
handleMessageClick(foundResult.message);
setFocusedIndex(index + viewportOffset);
}
}, `.${RESULT_ITEM_CLASS_NAME}`, true);
const handleKeyDown = useKeyboardListNavigation(
containerRef,
hasResultsContainer || hasMemberResults,
(index) => {
const actualIndex = index === -1 ? 0 : index;
const membersCount = displayedMembers?.length || 0;
// Member selection
if (actualIndex < membersCount && displayedMembers) {
const memberId = displayedMembers[actualIndex];
if (memberId) {
handleSelectFromMember(memberId);
}
return;
}
// Message selection
const messageIndex = actualIndex - membersCount;
const foundResult = viewportResults?.[messageIndex];
if (foundResult) {
handleMessageClick(foundResult.message);
setFocusedIndex(messageIndex + viewportOffset);
}
},
`.${RESULT_ITEM_CLASS_NAME}`,
true,
);
const updateSearchParams = useLastCallback((update: Partial<MiddleSearchParams>) => {
updateMiddleSearch({ chatId: chat!.id, threadId, update });
@ -474,6 +554,18 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
});
const handleDeleteTag = useLastCallback(() => {
if (fromPeerId) {
updateSearchParams({ fromPeerId: undefined });
setMemberSearchQuery('');
return;
}
if (isFromFilterMode) {
setIsFromFilterMode(false);
setMemberSearchQuery('');
return;
}
if (isHashtagQuery) {
updateSearchParams({ isHashtag: false });
return;
@ -489,6 +581,19 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
setIsViewAsList(true);
});
const handleFromFilterClick = useLastCallback(() => {
setIsFromFilterMode(true);
setMemberSearchQuery('');
focusInput();
});
const handleSelectFromMember = useLastCallback((peerId: string) => {
updateMiddleSearch({ chatId: chat!.id, threadId, update: { fromPeerId: peerId } });
setMemberSearchQuery('');
setQuery('');
focusInput();
});
const handleFocusOlder = useLastCallback(() => {
if (searchType !== 'chat') return;
markBlurred();
@ -551,11 +656,25 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
return undefined;
}
function renderMemberItem(peerId: string) {
const peer = selectPeer(getGlobal(), peerId);
if (!peer) return undefined;
return (
<MiddleSearchResult
key={`member-${peerId}`}
className={RESULT_ITEM_CLASS_NAME}
peer={peer}
onClick={handleSelectFromMember}
/>
);
}
function renderDropdown() {
return (
<div className={buildClassName(styles.dropdown, !hasResultsDropdown && styles.dropdownHidden)}>
{!isMobile && <div className={styles.separator} />}
{hasSavedTags && !isHashtagQuery && (
{hasSavedTags && !isHashtagQuery && !hasMemberDropdown && (
<div
className={buildClassName(
styles.savedTags,
@ -580,7 +699,7 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
})}
</div>
)}
{isHashtagQuery && (
{isHashtagQuery && !hasMemberDropdown && (
<div
className={buildClassName(styles.searchTypes, 'no-scrollbar')}
>
@ -589,16 +708,21 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
{renderTypeTag('channels')}
</div>
)}
{hasResultsContainer && (
{Boolean(hasResultsContainer || hasMemberResults) && (
<InfiniteScroll
ref={containerRef}
className={buildClassName(styles.results, 'custom-scroll')}
items={viewportResults}
items={hasMemberDropdown ? displayedMembers : viewportResults}
itemSelector={`.${RESULT_ITEM_CLASS_NAME}`}
preloadBackwards={0}
onLoadMore={getMore}
onLoadMore={hasMemberDropdown ? getMoreMembers : getMore}
onKeyDown={handleKeyDown}
>
{isMemberSearchLoading && hasMemberDropdown && <Loading />}
{displayedMembers?.map(renderMemberItem)}
{hasMemberDropdown && !isMemberSearchLoading && !memberSearchResults?.length && (
<span className={styles.placeholder}>{oldLang('NoResult')}</span>
)}
{areResultsEmpty && (
<span key="nothing" className={styles.placeholder}>
{oldLang('NoResultFoundFor', query)}
@ -609,7 +733,7 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
{oldLang('HashtagSearchPlaceholder')}
</span>
)}
{viewportResults?.map(({
{!hasMemberDropdown && viewportResults?.map(({
message, senderPeer, messageChat, searchResultKey,
}, i) => (
<MiddleSearchResult
@ -618,7 +742,7 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
className={RESULT_ITEM_CLASS_NAME}
query={query}
message={message}
senderPeer={senderPeer}
peer={senderPeer}
messageChat={messageChat}
shouldShowChat={isHashtagQuery}
isActive={focusedIndex - viewportOffset === i}
@ -654,7 +778,7 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
)}
<SearchInput
ref={inputRef}
value={query}
value={isFromFilterMode && !fromPeerId ? memberSearchQuery : query}
className={buildClassName(
styles.input,
hasResultsDropdown && styles.withDropdown,
@ -687,11 +811,27 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
/>
)}
{isHashtagQuery && <div className={styles.hash}>#</div>}
{(isFromFilterMode || fromPeerId) && (
<div className={styles.fromTag}>
{lang('SearchFilterFrom')}
{fromPeerId && <PeerChip peerId={fromPeerId} forceShowSelf canClose onClick={handleDeleteTag} />}
</div>
)}
</div>
{!isMobile && renderDropdown()}
</SearchInput>
{!isMobile && (
<div className={styles.icons}>
{isGroupChat && !isFromFilterMode && !fromPeerId && (
<Button
round
size="smaller"
color="translucent"
onClick={handleFromFilterClick}
ariaLabel={oldLang('FilterByUser')}
iconName="user"
/>
)}
<Button
round
size="smaller"
@ -706,6 +846,16 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
{isMobile && renderDropdown()}
{isMobile && (
<div className={styles.footer}>
{isGroupChat && !isFromFilterMode && !fromPeerId && (
<Button
round
size="smaller"
color="translucent"
onClick={handleFromFilterClick}
ariaLabel={oldLang('FilterByUser')}
iconName="user"
/>
)}
<Button
round
size="smaller"
@ -779,7 +929,7 @@ export default memo(withGlobal<OwnProps>(
}
const {
requestedQuery, savedTag, results, fetchingQuery, isHashtag, type,
requestedQuery, savedTag, results, fetchingQuery, isHashtag, type, fromPeerId,
} = selectCurrentMiddleSearch(global) || {};
const { totalCount, foundIds, query: lastSearchQuery } = results || {};
@ -808,6 +958,8 @@ export default memo(withGlobal<OwnProps>(
currentUserId,
searchType: type,
lastSearchQuery,
fromPeerId,
isGroupChat: isChatGroup(chat),
};
},
)(MiddleSearch));

View File

@ -2,7 +2,8 @@ import { memo } from '../../../lib/teact/teact';
import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types';
import { getMessageSenderName } from '../../../global/helpers/peers';
import { getGroupStatus, getMainUsername } from '../../../global/helpers';
import { getMessageSenderName, isApiPeerChat } from '../../../global/helpers/peers';
import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText';
@ -18,13 +19,13 @@ import styles from './MiddleSearchResult.module.scss';
type OwnProps = {
isActive?: boolean;
message: ApiMessage;
senderPeer?: ApiPeer;
message?: ApiMessage;
peer?: ApiPeer;
messageChat?: ApiChat;
shouldShowChat?: boolean;
query?: string;
className?: string;
onClick: (message: ApiMessage) => void;
onClick: (clickArg: any) => void;
};
const TRUNCATE_LENGTH = 200;
@ -32,7 +33,7 @@ const TRUNCATE_LENGTH = 200;
const MiddleSearchResult = ({
isActive,
message,
senderPeer,
peer,
messageChat,
shouldShowChat,
query,
@ -40,11 +41,47 @@ const MiddleSearchResult = ({
onClick,
}: OwnProps) => {
const lang = useLang();
if (peer && !message) {
const username = getMainUsername(peer);
const handlePeerClick = () => {
onClick(peer.id);
};
return (
<div
role="button"
tabIndex={0}
className={buildClassName(styles.root, isActive && styles.active, className)}
onClick={handlePeerClick}
>
<Avatar
className={styles.avatar}
peer={peer}
size="medium"
/>
<div className={styles.info}>
<div className={styles.topRow}>
<FullNameTitle peer={peer} withEmojiStatus />
</div>
{(isApiPeerChat(peer) || username) && (
<div className={styles.subtitle} dir="auto">
{username
? `@${username}`
: getGroupStatus(lang, peer as ApiChat)}
</div>
)}
</div>
</div>
);
}
if (!message) return undefined;
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
const peer = shouldShowChat ? messageChat : senderPeer;
const senderName = shouldShowChat && senderPeer ? getMessageSenderName(lang, message.chatId, senderPeer) : undefined;
const senderPeer = shouldShowChat ? messageChat : peer;
const senderName = shouldShowChat && peer ? getMessageSenderName(lang, message.chatId, peer) : undefined;
const handleClick = useLastCallback(() => {
onClick(message);
@ -59,13 +96,13 @@ const MiddleSearchResult = ({
>
<Avatar
className={styles.avatar}
peer={peer}
peer={senderPeer}
text={hiddenForwardTitle}
size="medium"
/>
<div className={styles.info}>
<div className={styles.topRow}>
{(peer && <FullNameTitle peer={peer} withEmojiStatus />) || hiddenForwardTitle}
{(senderPeer && <FullNameTitle peer={senderPeer} withEmojiStatus />) || hiddenForwardTitle}
<LastMessageMeta className={styles.meta} message={message} />
</div>
<div className={styles.subtitle} dir="auto">

View File

@ -64,16 +64,17 @@ addActionHandler('performMiddleSearch', async (global, actions, payload): Promis
currentSearch = selectCurrentMiddleSearch(global, tabId)!;
const {
results, savedTag, type, isHashtag,
results, savedTag, type, isHashtag, fromPeerId,
} = currentSearch;
const shouldReuseParams = results?.query === query;
const fromPeer = fromPeerId ? selectPeer(global, fromPeerId) : undefined;
const offsetId = shouldReuseParams ? results?.nextOffsetId : undefined;
const offsetRate = shouldReuseParams ? results?.nextOffsetRate : undefined;
const offsetPeerId = shouldReuseParams ? results?.nextOffsetPeerId : undefined;
const offsetPeer = shouldReuseParams && offsetPeerId ? selectChat(global, offsetPeerId) : undefined;
const shouldHaveQuery = isHashtag || !savedTag;
const shouldHaveQuery = isHashtag || (!savedTag && !fromPeerId);
if (shouldHaveQuery && !query) {
global = updateMiddleSearch(global, realChatId, threadId, {
fetchingQuery: undefined,
@ -98,6 +99,7 @@ addActionHandler('performMiddleSearch', async (global, actions, payload): Promis
offsetId,
isSavedDialog,
savedTag,
fromPeer,
});
}

View File

@ -12,13 +12,13 @@ import {
import { selectCurrentMessageList } from '../../selectors';
addActionHandler('openMiddleSearch', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const { fromPeerId, tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
if (!chatId || !threadId) {
return undefined;
}
return updateMiddleSearch(global, chatId, threadId, {}, tabId);
return updateMiddleSearch(global, chatId, threadId, { fromPeerId }, tabId);
});
addActionHandler('closeMiddleSearch', (global, actions, payload): ActionReturnType => {

View File

@ -77,7 +77,11 @@ export function updateMiddleSearch<T extends GlobalState>(
updated.type = 'chat';
}
if (currentSearch && (currentSearch.type !== updated.type || currentSearch.savedTag !== updated.savedTag)) {
if (currentSearch && (
currentSearch.type !== updated.type
|| currentSearch.savedTag !== updated.savedTag
|| currentSearch.fromPeerId !== updated.fromPeerId
)) {
updated.results = undefined;
}

View File

@ -312,7 +312,9 @@ export interface ActionPayloads {
};
// Message search
openMiddleSearch: WithTabId | undefined;
openMiddleSearch: {
fromPeerId?: string;
} & WithTabId | undefined;
closeMiddleSearch: WithTabId | undefined;
updateMiddleSearch: {
chatId: string;

View File

@ -1,7 +1,11 @@
import { useState } from '../lib/teact/teact';
import { getGlobal } from '../global';
import type { ApiChat } from '../api/types';
import { isChatBasicGroup, isChatSuperGroup } from '../global/helpers';
import { filterPeersByQuery } from '../global/helpers/peers';
import { selectChatFullInfo } from '../global/selectors';
import { callApi } from '../api/gramjs';
import useAsync from './useAsync';
import useDebouncedMemo from './useDebouncedMemo';
@ -20,13 +24,40 @@ export async function peerGlobalSearch(query: string) {
export function prepareChatMemberSearch(chat: ApiChat) {
return async (query: string) => {
const trimmedQuery = query.trim();
// For basic groups, filter from cached members in fullInfo
if (isChatBasicGroup(chat)) {
const global = getGlobal();
const fullInfo = selectChatFullInfo(global, chat.id);
const memberIds = fullInfo?.members?.map((m) => m.userId) || [];
if (!trimmedQuery) {
return memberIds;
}
return filterPeersByQuery({ ids: memberIds, query: trimmedQuery, type: 'user' });
}
// For supergroups/channels, use API
const searchResult = await callApi('fetchMembers', {
chat,
memberFilter: 'search',
query,
memberFilter: trimmedQuery ? 'search' : 'recent',
query: trimmedQuery,
});
return searchResult?.members?.map((member) => member.userId) || [];
const memberIds = searchResult?.members?.map((member) => member.userId) || [];
if (!isChatSuperGroup(chat)) {
return memberIds;
}
if (!trimmedQuery) {
return [...memberIds, chat.id];
}
const chatMatches = filterPeersByQuery({ ids: [chat.id], query: trimmedQuery, type: 'chat' });
return [...memberIds, ...chatMatches];
};
}

View File

@ -411,6 +411,7 @@ export type MiddleSearchParams = {
requestedQuery?: string;
savedTag?: ApiReaction;
isHashtag?: boolean;
fromPeerId?: string;
fetchingQuery?: string;
type: MiddleSearchType;
results?: MiddleSearchResults;

View File

@ -1398,6 +1398,7 @@ export interface LangPair {
'PrivateChatsSearchContext': undefined;
'GroupChatsSearchContext': undefined;
'ChannelsSearchContext': undefined;
'SearchFilterFrom': undefined;
'FolderLinkSubtitleNew': undefined;
'FolderLinkSubtitleAlready': undefined;
'FolderLinkAddFolder': undefined;