import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo, useRef, } from '../../lib/teact/teact'; import { getGlobal, withGlobal } from '../../global'; import type { ApiAvailableReaction, ApiReaction, ApiReactionWithPaid, ApiSticker, ApiStickerSet, } from '../../api/types'; import type { StickerSetOrReactionsSetOrRecent } from '../../types'; import { FAVORITE_SYMBOL_SET_ID, POPULAR_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, SLIDE_TRANSITION_DURATION, STICKER_PICKER_MAX_SHARED_COVERS, STICKER_SIZE_PICKER_HEADER, TOP_SYMBOL_SET_ID, } from '../../config'; import { isSameReaction } from '../../global/helpers'; import { selectCanPlayAnimatedEmojis, selectChatFullInfo, selectIsAlwaysHighPriorityEmoji, selectIsChatWithSelf, selectIsCurrentUserPremium, } from '../../global/selectors'; import animateHorizontalScroll from '../../util/animateHorizontalScroll'; import buildClassName from '../../util/buildClassName'; import { pickTruthy, unique } from '../../util/iteratees'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { REM } from './helpers/mediaDimensions'; import useAppLayout from '../../hooks/useAppLayout'; import useHorizontalScroll from '../../hooks/useHorizontalScroll'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import useScrolledState from '../../hooks/useScrolledState'; import useAsyncRendering from '../right/hooks/useAsyncRendering'; import { useStickerPickerObservers } from './hooks/useStickerPickerObservers'; import StickerSetCover from '../middle/composer/StickerSetCover'; import Button from '../ui/Button'; import Loading from '../ui/Loading'; import Icon from './icons/Icon'; import StickerButton from './StickerButton'; import StickerSet from './StickerSet'; import pickerStyles from '../middle/composer/StickerPicker.module.scss'; import styles from './CustomEmojiPicker.module.scss'; type OwnProps = { chatId?: string; className?: string; pickerListClassName?: string; isHidden?: boolean; loadAndPlay: boolean; idPrefix?: string; withDefaultTopicIcons?: boolean; selectedReactionIds?: string[]; isStatusPicker?: boolean; isReactionPicker?: boolean; isTranslucent?: boolean; onCustomEmojiSelect: (sticker: ApiSticker) => void; onReactionSelect?: (reaction: ApiReactionWithPaid) => void; onReactionContext?: (reaction: ApiReactionWithPaid) => void; onContextMenuOpen?: NoneToVoidFunction; onContextMenuClose?: NoneToVoidFunction; onContextMenuClick?: NoneToVoidFunction; }; type StateProps = { customEmojisById?: Record; recentCustomEmojiIds?: string[]; recentStatusEmojis?: ApiSticker[]; chatEmojiSetId?: string; topReactions?: ApiReaction[]; recentReactions?: ApiReaction[]; defaultTagReactions?: ApiReaction[]; stickerSetsById: Record; availableReactions?: ApiAvailableReaction[]; addedCustomEmojiIds?: string[]; defaultTopicIconsId?: string; defaultStatusIconsId?: string; customEmojiFeaturedIds?: string[]; canAnimate?: boolean; isSavedMessages?: boolean; isCurrentUserPremium?: boolean; isWithPaidReaction?: boolean; }; const HEADER_BUTTON_WIDTH = 2.5 * REM; // px (including margin) const DEFAULT_ID_PREFIX = 'custom-emoji-set'; const TOP_REACTIONS_COUNT = 16; const RECENT_REACTIONS_COUNT = 32; const RECENT_DEFAULT_STATUS_COUNT = 7; const FADED_BUTTON_SET_IDS = new Set([RECENT_SYMBOL_SET_ID, FAVORITE_SYMBOL_SET_ID, POPULAR_SYMBOL_SET_ID]); const STICKER_SET_IDS_WITH_COVER = new Set([ RECENT_SYMBOL_SET_ID, FAVORITE_SYMBOL_SET_ID, POPULAR_SYMBOL_SET_ID, ]); const CustomEmojiPicker: FC = ({ className, pickerListClassName, isHidden, loadAndPlay, addedCustomEmojiIds, customEmojisById, recentCustomEmojiIds, selectedReactionIds, recentStatusEmojis, stickerSetsById, chatEmojiSetId, topReactions, recentReactions, availableReactions, idPrefix = DEFAULT_ID_PREFIX, customEmojiFeaturedIds, canAnimate, isReactionPicker, isStatusPicker, isTranslucent, isSavedMessages, isCurrentUserPremium, withDefaultTopicIcons, defaultTopicIconsId, defaultStatusIconsId, defaultTagReactions, isWithPaidReaction, onCustomEmojiSelect, onReactionSelect, onReactionContext, onContextMenuOpen, onContextMenuClose, onContextMenuClick, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); // eslint-disable-next-line no-null/no-null const headerRef = useRef(null); // eslint-disable-next-line no-null/no-null const sharedCanvasRef = useRef(null); // eslint-disable-next-line no-null/no-null const sharedCanvasHqRef = useRef(null); const { isMobile } = useAppLayout(); const { handleScroll: handleContentScroll, isAtBeginning: shouldHideTopBorder, } = useScrolledState(); const recentCustomEmojis = useMemo(() => { return isStatusPicker ? recentStatusEmojis : Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!)); }, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]); const prefix = `${idPrefix}-custom-emoji`; const { activeSetIndex, observeIntersectionForSet, observeIntersectionForPlayingItems, observeIntersectionForShowingItems, observeIntersectionForCovers, selectStickerSet, } = useStickerPickerObservers(containerRef, headerRef, prefix, isHidden); const canLoadAndPlay = usePrevDuringAnimation(loadAndPlay || undefined, SLIDE_TRANSITION_DURATION); const lang = useOldLang(); const areAddedLoaded = Boolean(addedCustomEmojiIds); const allSets = useMemo(() => { const defaultSets: StickerSetOrReactionsSetOrRecent[] = []; if (isReactionPicker && isSavedMessages) { if (defaultTagReactions?.length) { defaultSets.push({ id: TOP_SYMBOL_SET_ID, accessHash: '', title: lang('PremiumPreviewTags'), reactions: defaultTagReactions, count: defaultTagReactions.length, isEmoji: true, }); } } if (isReactionPicker && !isSavedMessages) { const topReactionsSlice: ApiReactionWithPaid[] = topReactions?.slice(0, TOP_REACTIONS_COUNT) || []; if (isWithPaidReaction) { topReactionsSlice.unshift({ type: 'paid' }); } if (topReactionsSlice?.length) { defaultSets.push({ id: TOP_SYMBOL_SET_ID, accessHash: '', title: lang('Reactions'), reactions: topReactionsSlice, count: topReactionsSlice.length, isEmoji: true, }); } const cleanRecentReactions = (recentReactions || []) .filter((reaction) => !topReactionsSlice.some((topReaction) => isSameReaction(topReaction, reaction))) .slice(0, RECENT_REACTIONS_COUNT); const cleanAvailableReactions = (availableReactions || []) .filter(({ isInactive }) => !isInactive) .map(({ reaction }) => reaction) .filter((reaction) => { return !topReactionsSlice.some((topReaction) => isSameReaction(topReaction, reaction)) && !cleanRecentReactions.some((topReaction) => isSameReaction(topReaction, reaction)); }); if (cleanAvailableReactions?.length || cleanRecentReactions?.length) { const isPopular = !cleanRecentReactions?.length; const allRecentReactions = cleanRecentReactions.concat(cleanAvailableReactions); defaultSets.push({ id: isPopular ? POPULAR_SYMBOL_SET_ID : RECENT_SYMBOL_SET_ID, accessHash: '', title: lang(isPopular ? 'PopularReactions' : 'RecentStickers'), reactions: allRecentReactions, count: allRecentReactions.length, isEmoji: true, }); } } else if (isStatusPicker) { const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!]; if (defaultStatusIconsPack?.stickers?.length) { const stickers = defaultStatusIconsPack.stickers .slice(0, RECENT_DEFAULT_STATUS_COUNT) .concat(recentCustomEmojis || []); defaultSets.push({ ...defaultStatusIconsPack, stickers, count: stickers.length, id: RECENT_SYMBOL_SET_ID, title: lang('RecentStickers'), }); } } else if (withDefaultTopicIcons) { const defaultTopicIconsPack = stickerSetsById[defaultTopicIconsId!]; if (defaultTopicIconsPack.stickers?.length) { defaultSets.push({ ...defaultTopicIconsPack, id: RECENT_SYMBOL_SET_ID, title: lang('RecentStickers'), }); } } else if (recentCustomEmojis?.length) { defaultSets.push({ id: RECENT_SYMBOL_SET_ID, accessHash: '0', title: lang('RecentStickers'), stickers: recentCustomEmojis, count: recentCustomEmojis.length, isEmoji: true, }); } const userSetIds = [...(addedCustomEmojiIds || [])]; if (chatEmojiSetId) { userSetIds.unshift(chatEmojiSetId); } const setIdsToDisplay = unique(userSetIds.concat(customEmojiFeaturedIds || [])); const setsToDisplay = Object.values(pickTruthy(stickerSetsById, setIdsToDisplay)); return [ ...defaultSets, ...setsToDisplay, ]; }, [ addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis, customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, lang, recentReactions, defaultStatusIconsId, defaultTopicIconsId, isSavedMessages, defaultTagReactions, chatEmojiSetId, isWithPaidReaction, ]); const noPopulatedSets = useMemo(() => ( areAddedLoaded && allSets.filter((set) => set.stickers?.length).length === 0 ), [allSets, areAddedLoaded]); const canRenderContent = useAsyncRendering([], SLIDE_TRANSITION_DURATION); const shouldRenderContent = areAddedLoaded && canRenderContent && !noPopulatedSets; useHorizontalScroll(headerRef, isMobile || !shouldRenderContent); // Scroll container and header when active set changes useEffect(() => { if (!areAddedLoaded) { return; } const header = headerRef.current; if (!header) { return; } const newLeft = activeSetIndex * HEADER_BUTTON_WIDTH - (header.offsetWidth / 2 - HEADER_BUTTON_WIDTH / 2); animateHorizontalScroll(header, newLeft); }, [areAddedLoaded, activeSetIndex]); const handleEmojiSelect = useLastCallback((emoji: ApiSticker) => { onCustomEmojiSelect(emoji); }); function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) { const firstSticker = stickerSet.stickers?.[0]; const buttonClassName = buildClassName( pickerStyles.stickerCover, index === activeSetIndex && styles.activated, ); const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS; const isHq = selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet as ApiStickerSet); if (stickerSet.id === TOP_SYMBOL_SET_ID) { return undefined; } if (STICKER_SET_IDS_WITH_COVER.has(stickerSet.id) || stickerSet.hasThumbnail || !firstSticker) { const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === POPULAR_SYMBOL_SET_ID; const isFaded = FADED_BUTTON_SET_IDS.has(stickerSet.id); return ( ); } return ( ); } const fullClassName = buildClassName('StickerPicker', styles.root, className); if (!shouldRenderContent) { return (
{noPopulatedSets ? (
{lang('NoStickers')}
) : ( )}
); } const headerClassName = buildClassName( pickerStyles.header, 'no-scrollbar', !shouldHideTopBorder && pickerStyles.headerWithBorder, ); const listClassName = buildClassName( pickerStyles.main, pickerStyles.main_customEmoji, IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll', pickerListClassName, pickerStyles.hasHeader, ); return (
{allSets.map(renderCover)}
{allSets.map((stickerSet, i) => { const shouldHideHeader = stickerSet.id === TOP_SYMBOL_SET_ID || (stickerSet.id === RECENT_SYMBOL_SET_ID && (withDefaultTopicIcons || isStatusPicker)); const isChatEmojiSet = stickerSet.id === chatEmojiSetId; return ( = i - 1 && activeSetIndex <= i + 1} isSavedMessages={isSavedMessages} isStatusPicker={isStatusPicker} isReactionPicker={isReactionPicker} shouldHideHeader={shouldHideHeader} withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID} withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID} isChatEmojiSet={isChatEmojiSet} isCurrentUserPremium={isCurrentUserPremium} selectedReactionIds={selectedReactionIds} availableReactions={availableReactions} isTranslucent={isTranslucent} onReactionSelect={onReactionSelect} onReactionContext={onReactionContext} onStickerSelect={handleEmojiSelect} onContextMenuOpen={onContextMenuOpen} onContextMenuClose={onContextMenuClose} onContextMenuClick={onContextMenuClick} forcePlayback /> ); })}
); }; export default memo(withGlobal( (global, { chatId, isStatusPicker, isReactionPicker }): StateProps => { const { stickers: { setsById: stickerSetsById, }, customEmojis: { byId: customEmojisById, featuredIds: customEmojiFeaturedIds, statusRecent: { emojis: recentStatusEmojis, }, }, recentCustomEmojis: recentCustomEmojiIds, reactions: { availableReactions, recentReactions, topReactions, defaultTags, }, } = global; const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId)); const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined; return { customEmojisById: !isStatusPicker ? customEmojisById : undefined, recentCustomEmojiIds: !isStatusPicker ? recentCustomEmojiIds : undefined, recentStatusEmojis: isStatusPicker ? recentStatusEmojis : undefined, stickerSetsById, addedCustomEmojiIds: global.customEmojis.added.setIds, canAnimate: selectCanPlayAnimatedEmojis(global), isSavedMessages, isCurrentUserPremium: selectIsCurrentUserPremium(global), customEmojiFeaturedIds, defaultTopicIconsId: global.defaultTopicIconsId, defaultStatusIconsId: global.defaultStatusIconsId, topReactions: isReactionPicker ? topReactions : undefined, recentReactions: isReactionPicker ? recentReactions : undefined, chatEmojiSetId: chatFullInfo?.emojiSet?.id, isWithPaidReaction: isReactionPicker && chatFullInfo?.isPaidReactionAvailable, availableReactions: isReactionPicker ? availableReactions : undefined, defaultTagReactions: isReactionPicker ? defaultTags : undefined, }; }, )(CustomEmojiPicker));