import type { FC } from '@teact'; import { memo, useEffect, useMemo, useRef, useState, } from '@teact'; import type React from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiDisallowedGifts, ApiPeer, ApiPremiumGiftCodeOption, ApiStarGift, ApiStarGiftRegular, ApiStarsAmount, } from '../../../api/types'; import type { TabState } from '../../../global/types'; import type { StarGiftCategory } from '../../../types'; import { STARS_CURRENCY_CODE } from '../../../config'; import { getUserFullName } from '../../../global/helpers'; import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers'; import { selectTabState } from '../../../global/selectors'; import { selectPeer, selectUserFullInfo } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { throttle } from '../../../util/schedulers'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; import Avatar from '../../common/Avatar'; import InteractiveSparkles from '../../common/InteractiveSparkles'; import SafeLink from '../../common/SafeLink'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; import Transition from '../../ui/Transition'; import BalanceBlock from '../stars/BalanceBlock'; import GiftSendingOptions from './GiftComposer'; import GiftItemPremium from './GiftItemPremium'; import GiftItemStar from './GiftItemStar'; import GiftModalResaleScreen from './GiftModalResaleScreen'; import GiftResaleFilters from './GiftResaleFilters'; import StarGiftCategoryList from './StarGiftCategoryList'; import styles from './GiftModal.module.scss'; export type OwnProps = { modal: TabState['giftModal']; }; export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGift; type StateProps = { boostPerSentGift?: number; starGiftsById?: Record; starGiftIdsByCategory?: Record; starBalance?: ApiStarsAmount; peer?: ApiPeer; isSelf?: boolean; disallowedGifts?: ApiDisallowedGifts; resaleGiftsCount?: number; areResaleGiftsLoading?: boolean; selectedResaleGift?: ApiStarGift; tabId: number; }; const AVATAR_SIZE = 100; const INTERSECTION_THROTTLE = 200; const SCROLL_THROTTLE = 200; const AVATAR_SPARKLES_CENTER_SHIFT = [0, -50] as const; const runThrottledForScroll = throttle((cb) => cb(), SCROLL_THROTTLE, true); const GiftModal: FC = ({ modal, starGiftsById, starGiftIdsByCategory, starBalance, peer, isSelf, disallowedGifts, resaleGiftsCount, areResaleGiftsLoading, selectedResaleGift, tabId, }) => { const { closeGiftModal, openGiftInfoModal, resetResaleGifts, loadResaleGifts, openGiftInMarket, closeResaleGiftsMarket, } = getActions(); const dialogRef = useRef(); const transitionRef = useRef(); const giftHeaderRef = useRef(); const scrollerRef = useRef(); const isOpen = Boolean(modal); const renderingModal = useCurrentOrPrev(modal); const user = peer && isApiPeerUser(peer) ? peer : undefined; const chat = peer && isApiPeerChat(peer) ? peer : undefined; const [selectedGift, setSelectedGift] = useState(); const [shouldShowMainScreenHeader, setShouldShowMainScreenHeader] = useState(false); const [isMainScreenHeaderForStarGifts, setIsMainScreenHeaderForStarGifts] = useState(false); const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false); const [selectedCategory, setSelectedCategory] = useState('all'); const triggerSparklesRef = useRef<(() => void) | undefined>(); const areAllGiftsDisallowed = useMemo(() => { if (!disallowedGifts) { return undefined; } const { shouldDisallowPremiumGifts, ...disallowedGiftTypes } = disallowedGifts; return !isSelf && Object.values(disallowedGiftTypes).every(Boolean); }, [isSelf, disallowedGifts]); const areUnlimitedStarGiftsDisallowed = !isSelf && disallowedGifts?.shouldDisallowUnlimitedStarGifts; const areLimitedStarGiftsDisallowed = !isSelf && disallowedGifts?.shouldDisallowLimitedStarGifts; const oldLang = useOldLang(); const lang = useLang(); const allGifts = renderingModal?.gifts; const filteredGifts = useMemo(() => { return allGifts?.sort((prevGift, gift) => prevGift.months - gift.months) .filter((gift) => gift.users === 1 && gift.currency !== STARS_CURRENCY_CODE); }, [allGifts]); const giftsByStars = useMemo(() => { const mapGifts = new Map(); if (!filteredGifts) return mapGifts; filteredGifts.forEach((gift) => { const giftByStars = allGifts?.find( (starsGift) => starsGift.currency === STARS_CURRENCY_CODE && starsGift.months === gift.months, ); if (giftByStars) { mapGifts.set(gift, giftByStars); } }); return mapGifts; }, [allGifts, filteredGifts]); const baseGift = useMemo(() => { return filteredGifts?.reduce((prev, gift) => (prev.amount < gift.amount ? prev : gift)); }, [filteredGifts]); const { observe: observeIntersection, } = useIntersectionObserver({ rootRef: scrollerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen }); const isResaleScreen = Boolean(selectedResaleGift) && !selectedGift; const isGiftScreen = Boolean(selectedGift); const shouldShowHeader = isResaleScreen || isGiftScreen || shouldShowMainScreenHeader; const isHeaderForStarGifts = isGiftScreen ? isGiftScreenHeaderForStarGifts : isMainScreenHeaderForStarGifts; useEffect(() => { if (selectedResaleGift) { const giftId = 'regularGiftId' in selectedResaleGift ? selectedResaleGift.regularGiftId : selectedResaleGift.id; loadResaleGifts({ giftId }); } }, [selectedResaleGift]); useEffect(() => { if (!isOpen) { setShouldShowMainScreenHeader(false); setSelectedGift(undefined); setSelectedCategory('all'); } }, [isOpen, tabId, closeResaleGiftsMarket]); const handleScroll = useLastCallback((e: React.UIEvent) => { if (isGiftScreen) return; const currentTarget = e.currentTarget; runThrottledForScroll(() => { const { scrollTop } = currentTarget; setShouldShowMainScreenHeader(scrollTop > 150); if (transitionRef.current && giftHeaderRef.current) { const { top: headerTop } = giftHeaderRef.current.getBoundingClientRect(); const { top: transitionTop } = transitionRef.current.getBoundingClientRect(); setIsMainScreenHeaderForStarGifts(headerTop - transitionTop <= 0); } }); }); const giftPremiumDescription = lang('GiftPremiumDescription', { user: getUserFullName(user)!, link: ( ), }, { withNodes: true }); const starGiftDescription = chat ? lang('StarGiftDescriptionChannel', { peer: getPeerTitle(lang, chat) }, { withNodes: true, withMarkdown: true, }) : isSelf ? lang('StarGiftDescriptionSelf', undefined, { withNodes: true, renderTextFilters: ['br'], }) : lang('StarGiftDescription', { user: getUserFullName(user)!, }, { withNodes: true, withMarkdown: true }); function renderGiftPremiumHeader() { return (

{lang('GiftPremiumHeader')}

); } function renderGiftPremiumDescription() { return (

{giftPremiumDescription}

); } function renderStarGiftsHeader() { return (

{lang(isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader')}

); } function renderStarGiftsDescription() { return (

{starGiftDescription}

); } const handleGiftClick = useLastCallback((gift: GiftOption, target?: 'resell' | 'original') => { if (target === 'resell') { if (!('id' in gift)) { return; } if (isResaleScreen) { openGiftInfoModal({ gift, recipientId: renderingModal?.forPeerId }); return; } openGiftInMarket({ gift, tabId }); return; } setSelectedGift(gift); setIsGiftScreenHeaderForStarGifts('id' in gift); }); function renderStarGifts() { const filteredGiftIds = starGiftIdsByCategory?.[selectedCategory]?.filter((giftId) => { const gift = starGiftsById?.[giftId]; if (!gift) return false; const { isLimited, isSoldOut, upgradeStars } = gift; if (areUnlimitedStarGiftsDisallowed && !areLimitedStarGiftsDisallowed) { return isLimited; } if (areLimitedStarGiftsDisallowed && !areUnlimitedStarGiftsDisallowed) { return !isLimited && !isSoldOut; } if (areUnlimitedStarGiftsDisallowed && areLimitedStarGiftsDisallowed) { return Boolean(isLimited && Boolean(upgradeStars)); } return true; }); return (
{starGiftsById && filteredGiftIds?.flatMap((giftId) => { const gift = starGiftsById[giftId]; const shouldShowResale = selectedCategory !== 'stock' && Boolean(gift.availabilityResale); const shouldDuplicateAsResale = selectedCategory !== 'resale' && shouldShowResale && !gift.isSoldOut; const elements = [ , ]; if (shouldDuplicateAsResale) { elements.push( , ); } return elements; })}
); } function renderPremiumGifts() { return (
{filteredGifts?.map((gift) => { return ( ); })}
); } const onCategoryChanged = useLastCallback((category: StarGiftCategory) => { setSelectedCategory(category); }); const handleCloseModal = useLastCallback(() => { setSelectedGift(undefined); resetResaleGifts(); closeGiftModal(); }); const handleCloseButtonClick = useLastCallback(() => { if (isResaleScreen) { closeResaleGiftsMarket({ tabId }); return; } if (isGiftScreen) { setSelectedGift(undefined); return; } handleCloseModal(); }); const handleAvatarMouseMove = useLastCallback(() => { triggerSparklesRef.current?.(); }); const handleRequestAnimation = useLastCallback((animate: NoneToVoidFunction) => { triggerSparklesRef.current = animate; }); function renderMainScreen() { return (
{!isSelf && !chat && !disallowedGifts?.shouldDisallowPremiumGifts && ( <> {renderGiftPremiumHeader()} {renderGiftPremiumDescription()} {renderPremiumGifts()} )} {!areAllGiftsDisallowed && ( <> {renderStarGiftsHeader()} {renderStarGiftsDescription()} {renderStarGifts()} )}
); } const isBackButton = isGiftScreen || isResaleScreen; const buttonClassName = buildClassName( 'animated-close-icon', isBackButton && 'state-back', ); function renderHeader() { if (!shouldShowHeader) return undefined; if (isResaleScreen) { const isFirstLoading = areResaleGiftsLoading && !resaleGiftsCount; return (

{selectedResaleGift.title}

{isFirstLoading && (
{lang('Loading')}
)} {!isFirstLoading && resaleGiftsCount !== undefined && (
{lang('HeaderDescriptionResaleGifts', { count: resaleGiftsCount, }, { withNodes: true, withMarkdown: true, pluralValue: resaleGiftsCount })}
)}
); } return (

{lang(isHeaderForStarGifts ? (isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader') : 'GiftPremiumHeader')}

); } return (
{renderHeader()}
{!isGiftScreen && !isResaleScreen && renderMainScreen()} {isResaleScreen && selectedResaleGift && ( )} {isGiftScreen && renderingModal?.forPeerId && ( )}
); }; export default memo(withGlobal((global, { modal }): Complete => { const { starGifts, stars, currentUserId, } = global; const peer = modal?.forPeerId ? selectPeer(global, modal.forPeerId) : undefined; const isSelf = Boolean(currentUserId && modal?.forPeerId === currentUserId); const userFullInfo = peer ? selectUserFullInfo(global, peer?.id) : undefined; const { resaleGifts } = selectTabState(global); const resaleGiftsCount = resaleGifts.count; const areResaleGiftsLoading = resaleGifts.isLoading !== false; const selectedResaleGift = modal?.selectedResaleGift; return { boostPerSentGift: global.appConfig.boostsPerSentGift, starGiftsById: starGifts?.byId, starGiftIdsByCategory: starGifts?.idsByCategory, starBalance: stars?.balance, peer, isSelf, disallowedGifts: userFullInfo?.disallowedGifts, resaleGiftsCount, areResaleGiftsLoading, selectedResaleGift, tabId: selectTabState(global).id, }; })(GiftModal)); function getCategoryKey(category: StarGiftCategory) { if (category === 'all') return 0; if (category === 'limited') return 1; if (category === 'resale') return 2; if (category === 'stock') return 3; return category + 3; }