2025-01-05 20:18:43 +01:00

359 lines
10 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiPremiumGiftCodeOption,
ApiStarGiftRegular,
ApiStarsAmount,
ApiUser,
} from '../../../api/types';
import type { TabState } from '../../../global/types';
import type { StarGiftCategory } from '../../../types';
import { getUserFullName } from '../../../global/helpers';
import { selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
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 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 StarGiftCategoryList from './StarGiftCategoryList';
import styles from './GiftModal.module.scss';
import StarsBackground from '../../../assets/stars-bg.png';
export type OwnProps = {
modal: TabState['giftModal'];
};
export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGiftRegular;
type StateProps = {
boostPerSentGift?: number;
starGiftsById?: Record<string, ApiStarGiftRegular>;
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
starBalance?: ApiStarsAmount;
user?: ApiUser;
isSelf?: boolean;
};
const AVATAR_SIZE = 100;
const INTERSECTION_THROTTLE = 200;
const PremiumGiftModal: FC<OwnProps & StateProps> = ({
modal,
starGiftsById,
starGiftCategoriesByName,
starBalance,
user,
isSelf,
}) => {
const {
closeGiftModal, requestConfetti,
} = getActions();
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const transitionRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const giftHeaderRef = useRef<HTMLHeadingElement>(null);
// eslint-disable-next-line no-null/no-null
const scrollerRef = useRef<HTMLDivElement>(null);
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const [selectedGift, setSelectedGift] = useState<GiftOption | undefined>();
const [isHeaderHidden, setIsHeaderHidden] = useState(true);
const [isHeaderForStarGifts, setIsHeaderForStarGifts] = useState(false);
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
const oldLang = useOldLang();
const lang = useLang();
const filteredGifts = useMemo(() => {
return renderingModal?.gifts?.sort((prevGift, gift) => prevGift.months - gift.months)
.filter((gift) => gift.users === 1);
}, [renderingModal]);
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 showConfetti = useLastCallback(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
const {
top, left, width, height,
} = dialog.querySelector('.modal-content')!.getBoundingClientRect();
requestConfetti({
top,
left,
width,
height,
withStars: true,
});
}
});
useEffect(() => {
if (renderingModal?.isCompleted) {
showConfetti();
}
}, [renderingModal]);
useEffect(() => {
if (!isOpen) {
setIsHeaderHidden(true);
setSelectedGift(undefined);
}
}, [isOpen]);
const handleScroll = useLastCallback((e: React.UIEvent<HTMLDivElement>) => {
if (selectedGift) return;
const { scrollTop } = e.currentTarget;
setIsHeaderHidden(scrollTop <= 150);
if (transitionRef.current && giftHeaderRef.current) {
const { top: headerTop } = giftHeaderRef.current.getBoundingClientRect();
const { top: transitionTop } = transitionRef.current.getBoundingClientRect();
setIsHeaderForStarGifts(headerTop - transitionTop <= 0);
}
});
const giftPremiumDescription = lang('GiftPremiumDescription', {
user: getUserFullName(user)!,
link: (
<SafeLink
text={lang('GiftPremiumDescriptionLinkCaption')}
url={lang('GiftPremiumDescriptionLink')}
/>
),
}, { withNodes: true });
const starGiftDescription = isSelf
? lang('StarGiftDescriptionSelf', undefined, {
withNodes: true,
renderTextFilters: ['br'],
})
: lang('StarGiftDescription', {
user: getUserFullName(user)!,
}, { withNodes: true, withMarkdown: true });
function renderGiftPremiumHeader() {
return (
<h2 className={buildClassName(styles.headerText, styles.center)}>
{lang('GiftPremiumHeader')}
</h2>
);
}
function renderGiftPremiumDescription() {
return (
<p className={buildClassName(styles.description, styles.center)}>
{giftPremiumDescription}
</p>
);
}
function renderStarGiftsHeader() {
return (
<h2 ref={giftHeaderRef} className={buildClassName(styles.headerText, styles.center)}>
{lang(isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader')}
</h2>
);
}
function renderStarGiftsDescription() {
return (
<p className={buildClassName(styles.description, styles.starGiftsDescription, styles.center)}>
{starGiftDescription}
</p>
);
}
const handleGiftClick = useLastCallback((gift: GiftOption) => {
setSelectedGift(gift);
setIsHeaderForStarGifts('id' in gift);
setIsHeaderHidden(false);
});
function renderStarGifts() {
return (
<div className={styles.starGiftsContainer}>
{starGiftsById && starGiftCategoriesByName[selectedCategory].map((giftId) => {
const gift = starGiftsById[giftId];
return (
<GiftItemStar
gift={gift}
observeIntersection={observeIntersection}
onClick={handleGiftClick}
/>
);
})}
</div>
);
}
function renderPremiumGifts() {
return (
<div className={styles.premiumGiftsGallery}>
{filteredGifts?.map((gift) => {
return (
<GiftItemPremium
option={gift}
baseMonthAmount={baseGift ? Math.floor(baseGift.amount / baseGift.months) : undefined}
onClick={handleGiftClick}
/>
);
})}
</div>
);
}
const onCategoryChanged = useLastCallback((category: StarGiftCategory) => {
setSelectedCategory(category);
});
const handleCloseButtonClick = useLastCallback(() => {
if (selectedGift) {
setSelectedGift(undefined);
return;
}
closeGiftModal();
});
function renderMainScreen() {
return (
<div ref={scrollerRef} className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
<div className={styles.avatars}>
<Avatar
size={AVATAR_SIZE}
peer={user}
/>
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />
</div>
{!isSelf && renderGiftPremiumHeader()}
{!isSelf && renderGiftPremiumDescription()}
{!isSelf && renderPremiumGifts()}
{renderStarGiftsHeader()}
{renderStarGiftsDescription()}
<StarGiftCategoryList onCategoryChanged={onCategoryChanged} />
<Transition
name="zoomFade"
activeKey={getCategoryKey(selectedCategory)}
className={styles.starGiftsTransition}
>
{renderStarGifts()}
</Transition>
</div>
);
}
const isBackButton = Boolean(selectedGift);
const buttonClassName = buildClassName(
'animated-close-icon',
isBackButton && 'state-back',
);
return (
<Modal
dialogRef={dialogRef}
onClose={closeGiftModal}
isOpen={isOpen}
isSlim
contentClassName={styles.content}
className={buildClassName(styles.modalDialog, styles.root)}
>
<Button
className={styles.closeButton}
round
color="translucent"
size="smaller"
onClick={handleCloseButtonClick}
ariaLabel={isBackButton ? oldLang('Common.Back') : oldLang('Common.Close')}
>
<div className={buttonClassName} />
</Button>
<BalanceBlock className={styles.balance} balance={starBalance} />
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
<Transition
name="slideVerticalFade"
activeKey={Number(isHeaderForStarGifts)}
slideClassName={styles.headerSlide}
>
<h2 className={styles.commonHeaderText}>
{lang(isHeaderForStarGifts ? (isSelf ? 'StarsGiftHeaderSelf' : 'StarsGiftHeader') : 'GiftPremiumHeader')}
</h2>
</Transition>
</div>
<Transition
ref={transitionRef}
className={styles.transition}
name="pushSlide"
activeKey={selectedGift ? 1 : 0}
>
{!selectedGift && renderMainScreen()}
{selectedGift && renderingModal?.forUserId && (
<GiftSendingOptions gift={selectedGift} userId={renderingModal.forUserId} />
)}
</Transition>
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
const {
starGiftsById,
starGiftCategoriesByName,
stars,
currentUserId,
} = global;
const user = modal?.forUserId ? selectUser(global, modal.forUserId) : undefined;
const isSelf = Boolean(currentUserId && modal?.forUserId === currentUserId);
return {
boostPerSentGift: global.appConfig?.boostsPerSentGift,
starGiftsById,
starGiftCategoriesByName,
starBalance: stars?.balance,
user,
isSelf,
};
})(PremiumGiftModal));
function getCategoryKey(category: StarGiftCategory) {
if (category === 'all') return -2;
if (category === 'stock') return -1;
if (category === 'limited') return 0;
return category;
}