2026-01-20 12:07:34 +01:00

966 lines
30 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import {
memo, useEffect, useLayoutEffect,
useMemo,
useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type {
ApiChat, ApiMessage, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
} from '../../../api/types';
import type {
CustomPeer, MiddleSearchParams, MiddleSearchType, ThreadId,
} from '../../../types';
import { ANONYMOUS_USER_ID } from '../../../config';
import { requestMeasure, requestMutation, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
import {
getIsSavedDialog, getReactionKey, isChatGroup, isSameReaction, isSystemBot,
} from '../../../global/helpers';
import {
selectChat,
selectChatMessage,
selectCurrentMessageList,
selectCurrentMiddleSearch,
selectForwardedSender,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectMonoforumChannel,
selectPeer,
selectSender,
selectTabState,
} from '../../../global/selectors';
import { IS_IOS } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { getDayStartAt } from '../../../util/dates/dateFormat';
import focusEditableElement from '../../../util/focusEditableElement';
import focusNoScroll from '../../../util/focusNoScroll';
import { getSearchResultKey, parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { debounce, fastRaf } from '../../../util/schedulers';
import { useClickOutside } from '../../../hooks/events/useOutsideClick';
import useAppLayout from '../../../hooks/useAppLayout';
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
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';
import styles from './MiddleSearch.module.scss';
export type OwnProps = {
isActive: boolean;
};
type StateProps = {
chat?: ApiChat;
monoforumChat?: ApiChat;
threadId?: ThreadId;
requestedQuery?: string;
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
savedTag?: ApiReaction;
totalCount?: number;
lastSearchQuery?: string;
foundIds?: SearchResultKey[];
isHistoryCalendarOpen?: boolean;
isCurrentUserPremium?: boolean;
isSavedMessages?: boolean;
fetchingQuery?: string;
isHashtagQuery?: boolean;
searchType?: MiddleSearchType;
currentUserId?: string;
fromPeerId?: string;
isGroupChat?: boolean;
};
const CHANNELS_PEER: CustomPeer = {
isCustomPeer: true,
avatarIcon: 'channel-filled',
titleKey: 'SearchPublicPosts',
};
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);
const MiddleSearch: FC<OwnProps & StateProps> = ({
isActive,
chat,
monoforumChat,
threadId,
requestedQuery,
savedTags,
savedTag,
totalCount,
lastSearchQuery,
foundIds,
isHistoryCalendarOpen,
isCurrentUserPremium,
isSavedMessages,
fetchingQuery,
isHashtagQuery,
searchType = 'chat',
currentUserId,
fromPeerId,
isGroupChat,
}) => {
const {
updateMiddleSearch,
resetMiddleSearch,
performMiddleSearch,
focusMessage,
closeMiddleSearch,
openHistoryCalendar,
openPremiumModal,
loadSavedReactionTags,
} = getActions();
const ref = useRef<HTMLDivElement>();
const inputRef = useRef<HTMLInputElement>();
const containerRef = useRef<HTMLDivElement>();
const shouldCancelSearchRef = useRef(false);
const { isMobile } = useAppLayout();
const oldLang = useOldLang();
const lang = useLang();
const [query, setQuery] = useState(requestedQuery || '');
const [focusedIndex, setFocusedIndex] = useState(0);
const canFocusNewer = foundIds && focusedIndex > 0;
const canFocusOlder = foundIds && focusedIndex < foundIds.length - 1;
const [isFullyHidden, setIsFullyHidden] = useState(!isActive);
const hiddenTimerRef = useRef<number>();
const maybeLongPressActiveRef = useRef(true);
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;
// Ignore clicks inside modals
if ((event.target as HTMLElement).closest('.Modal')) return;
markBlurred();
});
useClickOutside([ref], handleClickOutside);
const hasResultsContainer = Boolean(((query || fromPeerId) && foundIds) || isHashtagQuery);
const isOnlyHash = isHashtagQuery && !query;
const isNonFocusedDropdownForced = searchType === 'myChats' || searchType === 'channels';
const hasMemberResults = !isHashtagQuery && Boolean(memberSearchResults?.length);
const hasQueryData = Boolean((query && !isOnlyHash) || savedTag || fromPeerId);
const hasNavigationButtons = searchType === 'chat' && Boolean(foundIds?.length);
const handleClose = useLastCallback(() => {
closeMiddleSearch();
});
const focusInput = useLastCallback(() => {
requestMeasure(() => {
focusNoScroll(inputRef.current);
});
});
const blurInput = useLastCallback(() => {
inputRef.current?.blur();
});
// Fix for iOS keyboard
useEffect(() => {
const { visualViewport } = window;
if (!visualViewport) {
return undefined;
}
const mainEl = document.getElementById('Main') as HTMLDivElement;
const handleResize = () => {
const { activeElement } = document;
if (activeElement && (activeElement === inputRef.current)) {
const { pageTop, height } = visualViewport;
requestMutation(() => {
mainEl.style.transform = `translateY(${pageTop}px)`;
mainEl.style.height = `${height}px`;
document.documentElement.scrollTop = pageTop;
});
} else {
requestMutation(() => {
mainEl.style.transform = '';
mainEl.style.height = '';
});
}
};
visualViewport.addEventListener('resize', handleResize);
return () => {
visualViewport.removeEventListener('resize', handleResize);
};
}, []);
// Reset focus on query result
useEffect(() => {
setFocusedIndex(-1);
}, [lastSearchQuery]);
// Disable native up/down buttons on iOS
useLayoutEffect(() => {
if (!IS_IOS) return;
Array.from(document.querySelectorAll<HTMLInputElement>('input')).forEach((input) => {
input.disabled = Boolean(isActive && input !== inputRef.current);
});
}, [isActive]);
// Blur on exit
useEffect(() => {
if (!isActive) {
inputRef.current!.blur();
setIsViewAsList(true);
setFocusedIndex(0);
setQuery('');
setIsFromFilterMode(false);
setMemberSearchQuery('');
hiddenTimerRef.current = window.setTimeout(() => setIsFullyHidden(true), HIDE_TIMEOUT);
} else {
setIsFullyHidden(false);
clearTimeout(hiddenTimerRef.current);
}
}, [isActive]);
useEffect(() => {
if (!requestedQuery || !chat?.id) return;
setQuery(requestedQuery);
updateMiddleSearch({ chatId: chat.id, threadId, update: { requestedQuery: undefined } });
setIsLoading(true);
requestNextMutation(() => {
const input = inputRef.current;
if (!input) return;
focusEditableElement(input, true, true);
markFocused();
});
}, [chat?.id, requestedQuery, threadId]);
useEffectWithPrevDeps(([prevIsActive]) => {
if (isActive !== prevIsActive && !query && lastSearchQuery) {
setQuery(lastSearchQuery); // Restore query when returning back
}
}, [isActive, lastSearchQuery, query]);
useEffectWithPrevDeps(([prevIsCalendarOpen]) => {
if (!isActive || prevIsCalendarOpen === isHistoryCalendarOpen) return;
if (isHistoryCalendarOpen) {
blurInput();
markBlurred();
} else {
focusInput();
}
}, [isHistoryCalendarOpen, isActive]);
const handleReset = useLastCallback(() => {
if (!query?.length && !savedTag && !fromPeerId && !isFromFilterMode) {
handleClose();
return;
}
setQuery('');
setIsLoading(false);
setIsFromFilterMode(false);
setMemberSearchQuery('');
resetMiddleSearch();
focusInput();
});
useEffect(() => (isActive ? captureEscKeyListener(handleReset) : undefined), [isActive, handleClose]);
const savedTagsArray = useMemo(() => {
if (!savedTags) return undefined;
return Object.values(savedTags);
}, [savedTags]);
const hasSavedTags = Boolean(savedTagsArray?.length);
const areSavedTagsDisabled = hasSavedTags && !isCurrentUserPremium;
useEffect(() => {
if (isSavedMessages && isActive) loadSavedReactionTags();
}, [isSavedMessages, isActive]);
const handleSearch = useLastCallback(() => {
const chatId = chat?.id;
if (!chatId) {
return;
}
runDebouncedForSearch(() => {
if (shouldCancelSearchRef.current) return;
performMiddleSearch({ chatId, threadId, query });
});
});
const handleQueryChange = useLastCallback((newQuery: string) => {
shouldCancelSearchRef.current = false;
if (newQuery.startsWith('#') && !isHashtagQuery) {
updateMiddleSearch({ chatId: chat!.id, threadId, update: { isHashtag: true } });
setQuery(newQuery.slice(1));
handleSearch();
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 && !fromPeerId) {
setIsLoading(false);
resetMiddleSearch();
shouldCancelSearchRef.current = true;
}
});
useEffect(() => {
if (isActive && (query || fromPeerId)) {
handleSearch();
}
}, [isActive, query, fromPeerId]);
useEffect(() => {
setIsLoading(Boolean(fetchingQuery));
}, [fetchingQuery]);
useEffect(() => {
if (!foundIds?.length) return;
const isClose = foundIds.length - focusedIndex < FOCUSED_SEARCH_TRIGGER_OFFSET;
if (isClose) {
handleSearch();
}
}, [focusedIndex, foundIds?.length]);
useEffect(() => {
if (!isActive) return undefined;
maybeLongPressActiveRef.current = true;
function focus() {
inputRef.current?.focus();
markFocused();
fastRaf(() => {
maybeLongPressActiveRef.current = false;
});
}
function removeListeners() {
window.removeEventListener('touchend', focus);
window.removeEventListener('mouseup', focus);
fastRaf(() => {
maybeLongPressActiveRef.current = false;
});
}
window.addEventListener('touchend', focus);
window.addEventListener('mouseup', focus);
window.addEventListener('touchstart', removeListeners);
window.addEventListener('mousedown', removeListeners);
return () => {
removeListeners();
window.removeEventListener('touchstart', removeListeners);
window.removeEventListener('mousedown', removeListeners);
};
}, [isActive]);
useHistoryBack({
isActive,
onBack: handleClose,
});
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 && !fromPeerId) || !viewportIds?.length) {
return MEMO_EMPTY_ARRAY;
}
const global = getGlobal();
return viewportIds.map((searchResultKey) => {
const [chatId, id] = parseSearchResultKey(searchResultKey);
const message = selectChatMessage(global, chatId, id);
if (!message) {
return undefined;
}
const originalSender = (isSavedMessages || isSystemBot(chatId) || chatId === ANONYMOUS_USER_ID)
? selectForwardedSender(global, message) : undefined;
const messageSender = selectSender(global, message);
const messageChat = selectChat(global, message.chatId);
const senderPeer = originalSender || messageSender;
return {
searchResultKey,
message,
messageChat,
senderPeer,
};
}).filter(Boolean);
}, [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);
const index = foundIds?.indexOf(searchResultKey) || 0;
const realIndex = index + viewportOffset;
setFocusedIndex(realIndex);
if (searchType === 'chat') {
setIsViewAsList(false);
}
focusMessage({
chatId: message.chatId,
messageId: message.id,
threadId: !isHashtagQuery ? threadId : undefined,
});
markBlurred();
});
const handleTriggerViewStyle = useLastCallback(() => {
setIsViewAsList((prev) => !prev);
markFocused();
});
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 });
handleSearch();
});
const activateSearchTag = useLastCallback((tag: ApiReaction) => {
if (areSavedTagsDisabled) {
openPremiumModal({
initialSection: 'saved_tags',
});
return;
}
updateSearchParams({ savedTag: tag });
});
const removeSearchSavedTag = useLastCallback(() => {
updateSearchParams({ savedTag: undefined });
});
const handleDeleteTag = useLastCallback(() => {
if (fromPeerId) {
updateSearchParams({ fromPeerId: undefined });
setMemberSearchQuery('');
return;
}
if (isFromFilterMode) {
setIsFromFilterMode(false);
setMemberSearchQuery('');
return;
}
if (isHashtagQuery) {
updateSearchParams({ isHashtag: false });
return;
}
if (savedTag) {
removeSearchSavedTag();
}
});
const handleChangeSearchType = useLastCallback((type: MiddleSearchType) => {
updateSearchParams({ type });
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();
blurInput();
if (foundIds) {
const newFocusIndex = focusedIndex + 1;
const [chatId, messageId] = parseSearchResultKey(foundIds[newFocusIndex]);
focusMessage({ chatId, messageId, threadId });
setFocusedIndex(newFocusIndex);
}
});
const handleFocusNewer = useLastCallback(() => {
if (searchType !== 'chat') return;
markBlurred();
blurInput();
if (foundIds) {
const newFocusIndex = focusedIndex - 1;
const [chatId, messageId] = parseSearchResultKey(foundIds[newFocusIndex]);
focusMessage({ chatId, messageId, threadId });
setFocusedIndex(newFocusIndex);
}
});
function renderTypeTag(type: MiddleSearchType, isForTag?: boolean) {
const isSelected = !isForTag && searchType === type;
switch (type) {
case 'chat':
return (
<PeerChip
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
peerId={chat?.id}
title={oldLang('SearchThisChat')}
clickArg="chat"
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
/>
);
case 'myChats':
return (
<PeerChip
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
peerId={currentUserId}
forceShowSelf
title={oldLang('SearchMyMessages')}
clickArg="myChats"
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
/>
);
case 'channels':
return (
<PeerChip
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
customPeer={CHANNELS_PEER}
title={oldLang('SearchPublicPosts')}
clickArg="channels"
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
/>
);
}
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 && !hasMemberDropdown && (
<div
className={buildClassName(
styles.savedTags,
!isMobile && styles.wrap,
'no-scrollbar',
)}
>
{savedTagsArray.map((tag) => {
const isChosen = isSameReaction(tag.reaction, savedTag);
return (
<SavedTagButton
containerId="local-search"
key={getReactionKey(tag.reaction)}
reaction={tag.reaction}
tag={tag}
withCount
isDisabled={areSavedTagsDisabled}
isChosen={isChosen}
onClick={isChosen ? removeSearchSavedTag : activateSearchTag}
/>
);
})}
</div>
)}
{isHashtagQuery && !hasMemberDropdown && (
<div
className={buildClassName(styles.searchTypes, 'no-scrollbar')}
>
{renderTypeTag('chat')}
{renderTypeTag('myChats')}
{renderTypeTag('channels')}
</div>
)}
{Boolean(hasResultsContainer || hasMemberResults) && (
<InfiniteScroll
ref={containerRef}
className={buildClassName(styles.results, 'custom-scroll')}
items={hasMemberDropdown ? displayedMembers : viewportResults}
itemSelector={`.${RESULT_ITEM_CLASS_NAME}`}
preloadBackwards={0}
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)}
</span>
)}
{isOnlyHash && (
<span key="enterhash" className={styles.placeholder}>
{oldLang('HashtagSearchPlaceholder')}
</span>
)}
{!hasMemberDropdown && viewportResults?.map(({
message, senderPeer, messageChat, searchResultKey,
}, i) => (
<MiddleSearchResult
key={searchResultKey}
teactOrderKey={-message.date}
className={RESULT_ITEM_CLASS_NAME}
query={query}
message={message}
peer={senderPeer}
messageChat={messageChat}
shouldShowChat={isHashtagQuery}
isActive={focusedIndex - viewportOffset === i}
onClick={handleMessageClick}
/>
))}
</InfiniteScroll>
)}
</div>
);
}
return (
<div
id="MiddleSearch"
className={buildClassName(
styles.root,
isActive && styles.active,
!isActive && isFullyHidden && 'visually-hidden', // `display: none` would prevent focus on iOS
isFocused && styles.focused,
isMobile && styles.mobile,
)}
ref={ref}
>
<div className={styles.header}>
{!isMobile && (
<Avatar
className={styles.avatar}
peer={monoforumChat || chat}
size="medium"
isSavedMessages={isSavedMessages}
/>
)}
<SearchInput
ref={inputRef}
value={isFromFilterMode && !fromPeerId ? memberSearchQuery : query}
className={buildClassName(
styles.input,
hasResultsDropdown && styles.withDropdown,
hasResultsDropdown && !isMobile && styles.adaptSearchBorders,
)}
canClose={!isMobile}
isLoading={isLoading}
resultsItemSelector={`.${styles.results} .${RESULT_ITEM_CLASS_NAME}`}
hasUpButton={hasNavigationButtons && !isMobile}
hasDownButton={hasNavigationButtons && !isMobile}
placeholder={isHashtagQuery ? oldLang('SearchHashtagsHint') : oldLang('Search')}
teactExperimentControlled
onChange={handleQueryChange}
onStartBackspace={handleDeleteTag}
onReset={handleReset}
withBackIcon={isMobile}
onFocus={markFocused}
focused={isFocused}
onUpClick={canFocusOlder ? handleFocusOlder : undefined}
onDownClick={canFocusNewer ? handleFocusNewer : undefined}
>
<div className={styles.searchTags}>
{savedTag && (
<SavedTagButton
containerId="local-search-tags"
className={styles.savedSearchTag}
reaction={savedTag}
tag={savedTags![getReactionKey(savedTag)]}
onClick={removeSearchSavedTag}
/>
)}
{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"
color="translucent"
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
ariaLabel={oldLang('JumpToDate')}
iconName="calendar"
/>
</div>
)}
</div>
{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"
color="translucent"
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
ariaLabel={oldLang('JumpToDate')}
iconName="calendar"
/>
<div className={styles.counter}>
{hasQueryData && (
foundIds?.length ? (
oldLang('Of', [Math.max(focusedIndex + 1, 1), totalCount])
) : foundIds && !foundIds.length && (
oldLang('NoResult')
)
)}
</div>
{searchType === 'chat' && Boolean(foundIds?.length) && (
<Button
className={styles.viewStyle}
size="smaller"
isText
fluid
noForcedUpperCase
onClick={handleTriggerViewStyle}
>
{isViewAsList ? oldLang('SearchAsChat') : oldLang('SearchAsList')}
</Button>
)}
{hasNavigationButtons && !hasResultsDropdown && (
<div className={styles.mobileNavigation}>
<Button
className={buildClassName(styles.navigationButton, !canFocusOlder && styles.navigationDisabled)}
round
size="smaller"
color="secondary"
onClick={handleFocusOlder}
nonInteractive={!canFocusOlder}
ariaLabel={lang('AriaSearchOlderResult')}
iconName="up"
/>
<Button
className={buildClassName(styles.navigationButton, !canFocusNewer && styles.navigationDisabled)}
round
size="smaller"
color="secondary"
onClick={handleFocusNewer}
nonInteractive={!canFocusNewer}
ariaLabel={lang('AriaSearchNewerResult')}
iconName="down"
/>
</div>
)}
</div>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const currentMessageList = selectCurrentMessageList(global);
if (!currentMessageList) {
return {} as Complete<StateProps>;
}
const { chatId, threadId } = currentMessageList;
const chat = selectChat(global, chatId);
if (!chat) {
return {} as Complete<StateProps>;
}
const {
requestedQuery, savedTag, results, fetchingQuery, isHashtag, type, fromPeerId,
} = selectCurrentMiddleSearch(global) || {};
const { totalCount, foundIds, query: lastSearchQuery } = results || {};
const currentUserId = global.currentUserId;
const isSavedMessages = selectIsChatWithSelf(global, chatId);
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
const monoforumChat = selectMonoforumChannel(global, chatId);
return {
chat,
monoforumChat,
requestedQuery,
totalCount,
threadId,
foundIds,
isHistoryCalendarOpen: Boolean(selectTabState(global).historyCalendarSelectedAt),
savedTags,
savedTag,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
isSavedMessages,
fetchingQuery,
isHashtagQuery: isHashtag,
currentUserId,
searchType: type,
lastSearchQuery,
fromPeerId,
isGroupChat: isChatGroup(chat),
};
},
)(MiddleSearch));