TelegramPWA/src/components/right/RightSearch.tsx
2024-02-23 14:06:06 +01:00

291 lines
8.2 KiB
TypeScript

import type { FC } from '../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useRef,
} from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
ApiMessage, ApiPeer, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
} from '../../api/types';
import type { ThreadId } from '../../types';
import { ANONYMOUS_USER_ID, REPLIES_USER_ID } from '../../config';
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../global/helpers';
import {
selectChatMessages,
selectCurrentTextSearch,
selectForwardedSender,
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectSender,
} from '../../global/selectors';
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import { debounce } from '../../util/schedulers';
import { renderMessageSummary } from '../common/helpers/renderMessageText';
import useHistoryBack from '../../hooks/useHistoryBack';
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import Avatar from '../common/Avatar';
import FullNameTitle from '../common/FullNameTitle';
import LastMessageMeta from '../common/LastMessageMeta';
import SavedTagButton from '../middle/message/reactions/SavedTagButton';
import InfiniteScroll from '../ui/InfiniteScroll';
import ListItem from '../ui/ListItem';
import './RightSearch.scss';
export type OwnProps = {
chatId: string;
threadId: ThreadId;
onClose: NoneToVoidFunction;
isActive: boolean;
};
type StateProps = {
messagesById?: Record<number, ApiMessage>;
query?: string;
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
searchTag?: ApiReaction;
totalCount?: number;
foundIds?: number[];
isSavedMessages?: boolean;
isCurrentUserPremium?: boolean;
};
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
const RightSearch: FC<OwnProps & StateProps> = ({
chatId,
threadId,
isActive,
messagesById,
query,
totalCount,
foundIds,
savedTags,
searchTag,
isSavedMessages,
isCurrentUserPremium,
onClose,
}) => {
const {
searchTextMessagesLocal,
setLocalTextSearchTag,
focusMessage,
openPremiumModal,
loadSavedReactionTags,
} = getActions();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const tagsRef = useRef<HTMLDivElement>(null);
const lang = useLang();
useHistoryBack({
isActive,
onBack: onClose,
});
useEffect(() => {
if (!isActive) {
return undefined;
}
disableDirectTextInput();
return enableDirectTextInput;
}, [isActive]);
const tags = useMemo(() => {
if (!savedTags) return undefined;
return Object.values(savedTags);
}, [savedTags]);
const hasTags = Boolean(tags?.length);
const areTagsDisabled = hasTags && !isCurrentUserPremium;
useHorizontalScroll(tagsRef, !hasTags);
useEffect(() => {
if (isActive) loadSavedReactionTags();
}, [hasTags, isActive]);
const handleSearchTextMessagesLocal = useLastCallback(() => {
runDebouncedForSearch(searchTextMessagesLocal);
});
const handleTagClick = useLastCallback((tag: ApiReaction) => {
if (areTagsDisabled) {
openPremiumModal({
initialSection: 'saved_tags',
});
return;
}
if (isSameReaction(tag, searchTag)) {
setLocalTextSearchTag({ tag: undefined });
return;
}
setLocalTextSearchTag({ tag });
handleSearchTextMessagesLocal();
});
const [viewportIds, getMore] = useInfiniteScroll(handleSearchTextMessagesLocal, foundIds);
const viewportResults = useMemo(() => {
if ((!query && !searchTag) || !viewportIds?.length || !messagesById) {
return MEMO_EMPTY_ARRAY;
}
return viewportIds.map((id) => {
const message = messagesById[id];
if (!message) {
return undefined;
}
const global = getGlobal();
const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID || chatId === ANONYMOUS_USER_ID)
? selectForwardedSender(global, message) : undefined;
const messageSender = selectSender(global, message);
const senderPeer = originalSender || messageSender;
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
return {
message,
senderPeer,
hiddenForwardTitle,
onClick: () => focusMessage({ chatId, threadId, messageId: id }),
};
}).filter(Boolean);
}, [query, searchTag, viewportIds, messagesById, isSavedMessages, chatId, threadId]);
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {
const foundResult = viewportResults?.[index === -1 ? 0 : index];
if (foundResult) {
foundResult.onClick();
}
}, '.ListItem-button', true);
const renderSearchResult = ({
message, senderPeer, hiddenForwardTitle, onClick,
}: {
message: ApiMessage;
senderPeer?: ApiPeer;
hiddenForwardTitle?: string;
onClick: NoneToVoidFunction;
}) => {
const text = renderMessageSummary(lang, message, undefined, query);
return (
<ListItem
key={message.id}
teactOrderKey={-message.date}
className="chat-item-clickable search-result-message m-0"
onClick={onClick}
>
<Avatar
peer={senderPeer}
text={hiddenForwardTitle}
/>
<div className="info">
<div className="search-result-message-top">
{senderPeer && <FullNameTitle peer={senderPeer} withEmojiStatus />}
{!senderPeer && hiddenForwardTitle}
<LastMessageMeta message={message} />
</div>
<div className="subtitle" dir="auto">
{text}
</div>
</div>
</ListItem>
);
};
const isOnTop = viewportIds?.[0] === foundIds?.[0];
return (
<InfiniteScroll
ref={containerRef}
className="RightSearch custom-scroll"
items={viewportResults}
preloadBackwards={0}
onLoadMore={getMore}
onKeyDown={handleKeyDown}
>
{hasTags && (
<div
ref={tagsRef}
className="search-tags custom-scroll-x no-scrollbar"
key="search-tags"
>
{tags.map((tag) => (
<SavedTagButton
containerId="local-search"
key={getReactionKey(tag.reaction)}
reaction={tag.reaction}
tag={tag}
withCount
isDisabled={areTagsDisabled}
isChosen={isSameReaction(tag.reaction, searchTag)}
onClick={handleTagClick}
/>
))}
</div>
)}
{isOnTop && (
<p key="helper-text" className="helper-text" dir="auto">
{!query ? (
lang('lng_dlg_search_for_messages')
) : (totalCount === 0 || !viewportResults.length) ? (
lang('lng_search_no_results')
) : totalCount === 1 ? (
'1 message found'
) : (
`${(viewportResults.length && (totalCount || viewportResults.length))} messages found`
)}
</p>
)}
{viewportResults.map(renderSearchResult)}
</InfiniteScroll>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId }): StateProps => {
const messagesById = selectChatMessages(global, chatId);
if (!messagesById) {
return {};
}
const { query, savedTag, results } = selectCurrentTextSearch(global) || {};
const { totalCount, foundIds } = results || {};
const isSavedMessages = selectIsChatWithSelf(global, chatId);
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
return {
messagesById,
query,
totalCount,
foundIds,
isSavedMessages,
savedTags,
searchTag: savedTag,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
},
)(RightSearch));