966 lines
30 KiB
TypeScript
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));
|