import { memo, useEffect, useMemo, useRef, useState } from '../../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../../global'; import type { ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributePattern, } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; import type { AnimationLevel } from '../../../../types'; import { selectAnimationLevel } from '../../../../global/selectors/sharedState'; import buildClassName from '../../../../util/buildClassName'; import { NEXT_ARROW_REPLACEMENT } from '../../../../util/localization/format'; import { resolveTransitionName } from '../../../../util/resolveTransitionName'; import { getGiftAttributes, getRandomGiftPreviewAttributes } from '../../../common/helpers/gifts'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; import useFlag from '../../../../hooks/useFlag'; import { useIntersectionObserver } from '../../../../hooks/useIntersectionObserver'; import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; import GiftRarityBadge from '../../../common/GiftRarityBadge'; import Button from '../../../ui/Button'; import InfiniteScroll from '../../../ui/InfiniteScroll'; import Link from '../../../ui/Link'; import Modal from '../../../ui/Modal'; import SquareTabList, { type TabWithProperties } from '../../../ui/SquareTabList'; import Transition from '../../../ui/Transition'; import GiftAttributeItem from '../GiftAttributeItem'; import UniqueGiftHeader from '../UniqueGiftHeader'; import styles from './GiftPreviewModal.module.scss'; export type OwnProps = { modal: TabState['giftPreviewModal']; }; type StateProps = { animationLevel: AnimationLevel; }; const MODEL_STICKER_SIZE = 80; const PATTERN_STICKER_SIZE = 60; const INTERSECTION_THROTTLE = 200; enum AttributeTab { Model, Backdrop, Pattern, } const GiftPreviewModal = ({ modal, animationLevel }: OwnProps & StateProps) => { const { closeGiftPreviewModal, openGiftInMarket, updateResaleGiftsFilter, } = getActions(); const [isCraftableModelsMode, showCraftableModels, showRegularModels] = useFlag(); const [isPlayingRandomPreviews, playRandomPreviews, stopRandomPreviews] = useFlag(true); const modelsContainerRef = useRef(); const patternsContainerRef = useRef(); const backdropsContainerRef = useRef(); const isOpen = Boolean(modal); const renderingModal = useCurrentOrPrev(modal); const originGift = renderingModal?.originGift; const initialAttributes = useMemo(() => originGift && getGiftAttributes(originGift), [originGift]); const lang = useLang(); const [selectedTabIndex, setSelectedTabIndex] = useState(AttributeTab.Model); const { regularModels, craftableModels, patterns, backdrops } = useMemo(() => { if (!renderingModal?.attributes) { return { regularModels: [], craftableModels: [], patterns: [], backdrops: [] }; } const result: { regularModels: ApiStarGiftAttributeModel[]; craftableModels: ApiStarGiftAttributeModel[]; patterns: ApiStarGiftAttributePattern[]; backdrops: ApiStarGiftAttributeBackdrop[]; } = { regularModels: [], craftableModels: [], patterns: [], backdrops: [] }; for (const attr of renderingModal.attributes) { if (attr.type === 'model') { if (attr.rarity.type === 'regular') { result.regularModels.push(attr); } else { result.craftableModels.push(attr); } } if (attr.type === 'pattern') result.patterns.push(attr); if (attr.type === 'backdrop') result.backdrops.push(attr); } return result; }, [renderingModal?.attributes]); const firstModel = regularModels[0]; const firstPattern = patterns[0]; const firstBackdrop = backdrops[0]; const [selectedModel, setSelectedModel] = useState(firstModel); const [selectedPattern, setSelectedPattern] = useState(firstPattern); const [selectedBackdrop, setSelectedBackdrop] = useState(firstBackdrop); useEffect(() => { if (isOpen) return; setSelectedTabIndex(AttributeTab.Model); showRegularModels(); }, [isOpen]); useEffect(() => { const newModel = initialAttributes?.model || firstModel; setSelectedModel(newModel); setSelectedPattern(initialAttributes?.pattern || firstPattern); setSelectedBackdrop(initialAttributes?.backdrop || firstBackdrop); if (newModel && newModel.rarity.type !== 'regular') showCraftableModels(); }, [initialAttributes, firstModel, firstPattern, firstBackdrop]); useEffect(() => { if (renderingModal?.shouldShowCraftableOnStart) { showCraftableModels(); } }, [renderingModal?.shouldShowCraftableOnStart]); const handleStickerAnimationEnded = useLastCallback((modelName: string) => { if (modelName !== selectedModel?.name || !isPlayingRandomPreviews) return; if (!originGift || !selectedModel || !selectedPattern || !selectedBackdrop) return; const attributesToUse = renderingModal?.shouldShowCraftableOnStart && isCraftableModelsMode ? renderingModal.attributes.filter((attr) => attr.type !== 'model' || attr.rarity.type !== 'regular') : renderingModal?.attributes; const newAttributes = getRandomGiftPreviewAttributes(attributesToUse, { model: selectedModel, pattern: selectedPattern, backdrop: selectedBackdrop, }); setSelectedModel(newAttributes.model); setSelectedPattern(newAttributes.pattern); setSelectedBackdrop(newAttributes.backdrop); }); const { observe: observeModelsIntersection, } = useIntersectionObserver({ rootRef: modelsContainerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: selectedTabIndex !== AttributeTab.Model, }); const { observe: observePatternsIntersection, } = useIntersectionObserver({ rootRef: patternsContainerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: selectedTabIndex !== AttributeTab.Pattern, }); const { observe: observeBackdropsIntersection, } = useIntersectionObserver({ rootRef: backdropsContainerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: selectedTabIndex !== AttributeTab.Backdrop, }); const tabs = useMemo(() => [ { title: lang('GiftAttributeModel') }, { title: lang('GiftAttributeBackdrop') }, { title: lang('GiftAttributeSymbol') }, ], [lang]); const handleClose = useLastCallback(() => closeGiftPreviewModal()); const handleSelectModel = useLastCallback((model: ApiStarGiftAttributeModel) => { setSelectedModel(model); stopRandomPreviews(); }); const handleSelectPattern = useLastCallback((pattern: ApiStarGiftAttributePattern) => { setSelectedPattern(pattern); stopRandomPreviews(); }); const handleSelectBackdrop = useLastCallback((backdrop: ApiStarGiftAttributeBackdrop) => { setSelectedBackdrop(backdrop); stopRandomPreviews(); }); const handleSymbolClick = useLastCallback(() => { if (!originGift || !selectedPattern) return; openGiftInMarket({ gift: originGift }); updateResaleGiftsFilter({ filter: { sortType: 'byDate', modelAttributes: [], backdropAttributes: [], patternAttributes: [{ type: 'pattern', documentId: selectedPattern.sticker.id, }], }, }); handleClose(); }); const handleBackdropClick = useLastCallback(() => { if (!originGift || !selectedBackdrop) return; openGiftInMarket({ gift: originGift }); updateResaleGiftsFilter({ filter: { sortType: 'byDate', modelAttributes: [], backdropAttributes: [{ type: 'backdrop', backdropId: selectedBackdrop.backdropId, }], patternAttributes: [], }, }); handleClose(); }); const handleModelClick = useLastCallback(() => { if (!originGift) return; openGiftInMarket({ gift: originGift }); updateResaleGiftsFilter({ filter: { sortType: 'byDate', modelAttributes: [{ type: 'model', documentId: selectedModel!.sticker.id, }], backdropAttributes: [], patternAttributes: [], }, }); handleClose(); }); const modalHeader = useMemo(() => { return ( <> ); } function renderTabContent() { switch (selectedTabIndex) { case AttributeTab.Model: return ( {lang( isCraftableModelsMode ? 'GiftPreviewCountCraftableModels' : 'GiftPreviewCountModels', { count: isCraftableModelsMode ? craftableModels.length : regularModels.length }, { pluralValue: isCraftableModelsMode ? craftableModels.length : regularModels.length, withNodes: true, withMarkdown: true, })} {Boolean(craftableModels?.length) && ( isCraftableModelsMode ? showRegularModels() : showCraftableModels()} > {lang( isCraftableModelsMode ? 'GiftPreviewToggleRegularModels' : 'GiftPreviewToggleCraftableModels', undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT }, )} )} )} items={isCraftableModelsMode ? craftableModels : regularModels} noFastList > {(isCraftableModelsMode ? craftableModels : regularModels).map((model) => ( ))} ); case AttributeTab.Pattern: return ( {lang('GiftPreviewCountPatterns', { count: patterns.length }, { pluralValue: patterns.length, withNodes: true, withMarkdown: true, })} )} items={patterns} noFastList > {patterns.map((pattern) => ( ))} ); case AttributeTab.Backdrop: return ( {lang('GiftPreviewCountBackdrops', { count: backdrops.length }, { pluralValue: backdrops.length, withNodes: true, withMarkdown: true, })} )} items={backdrops} noFastList > {backdrops.map((backdrop) => ( ))} ); default: return undefined; } } return ( {renderHeader()} {renderTabContent()} ); }; export default memo(withGlobal( (global): Complete => { return { animationLevel: selectAnimationLevel(global), }; }, )(GiftPreviewModal));