Search: Add sender filter (#6578)
This commit is contained in:
parent
3eb69cefb1
commit
224670674a
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -312,7 +312,9 @@ export interface ActionPayloads {
|
||||
};
|
||||
|
||||
// Message search
|
||||
openMiddleSearch: WithTabId | undefined;
|
||||
openMiddleSearch: {
|
||||
fromPeerId?: string;
|
||||
} & WithTabId | undefined;
|
||||
closeMiddleSearch: WithTabId | undefined;
|
||||
updateMiddleSearch: {
|
||||
chatId: string;
|
||||
|
||||
@ -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];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -411,6 +411,7 @@ export type MiddleSearchParams = {
|
||||
requestedQuery?: string;
|
||||
savedTag?: ApiReaction;
|
||||
isHashtag?: boolean;
|
||||
fromPeerId?: string;
|
||||
fetchingQuery?: string;
|
||||
type: MiddleSearchType;
|
||||
results?: MiddleSearchResults;
|
||||
|
||||
1
src/types/language.d.ts
vendored
1
src/types/language.d.ts
vendored
@ -1398,6 +1398,7 @@ export interface LangPair {
|
||||
'PrivateChatsSearchContext': undefined;
|
||||
'GroupChatsSearchContext': undefined;
|
||||
'ChannelsSearchContext': undefined;
|
||||
'SearchFilterFrom': undefined;
|
||||
'FolderLinkSubtitleNew': undefined;
|
||||
'FolderLinkSubtitleAlready': undefined;
|
||||
'FolderLinkAddFolder': undefined;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user