TelegramPWA/src/components/main/premium/PremiumFeatureModal.tsx

326 lines
12 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useRef, useState,
} from '../../../lib/teact/teact';
import type { ApiPremiumPromo } from '../../../api/types';
import type { ApiLimitType, GlobalState } from '../../../global/types';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
import useFlag from '../../../hooks/useFlag';
import renderText from '../../common/helpers/renderText';
import usePrevious from '../../../hooks/usePrevious';
import { formatCurrency } from '../../../util/formatCurrency';
import Button from '../../ui/Button';
import PremiumLimitPreview from './common/PremiumLimitPreview';
import PremiumFeaturePreviewVideo from './previews/PremiumFeaturePreviewVideo';
import PremiumFeaturePreviewReactions from './previews/PremiumFeaturePreviewReactions';
import SliderDots from '../../common/SliderDots';
import PremiumFeaturePreviewStickers from './previews/PremiumFeaturePreviewStickers';
import styles from './PremiumFeatureModal.module.scss';
export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
double_limits: 'PremiumPreviewLimits',
unique_reactions: 'PremiumPreviewReactions',
premium_stickers: 'PremiumPreviewStickers',
animated_emoji: 'PremiumPreviewEmoji',
no_ads: 'PremiumPreviewNoAds',
voice_to_text: 'PremiumPreviewVoiceToText',
profile_badge: 'PremiumPreviewProfileBadge',
faster_download: 'PremiumPreviewDownloadSpeed',
more_upload: 'PremiumPreviewUploads',
advanced_chat_management: 'PremiumPreviewAdvancedChatManagement',
animated_userpics: 'PremiumPreviewAnimatedProfiles',
};
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
double_limits: 'PremiumPreviewLimitsDescription',
unique_reactions: 'PremiumPreviewReactionsDescription',
premium_stickers: 'PremiumPreviewStickersDescription',
no_ads: 'PremiumPreviewNoAdsDescription',
animated_emoji: 'PremiumPreviewEmojiDescription',
voice_to_text: 'PremiumPreviewVoiceToTextDescription',
profile_badge: 'PremiumPreviewProfileBadgeDescription',
faster_download: 'PremiumPreviewDownloadSpeedDescription',
more_upload: 'PremiumPreviewUploadsDescription',
advanced_chat_management: 'PremiumPreviewAdvancedChatManagementDescription',
animated_userpics: 'PremiumPreviewAnimatedProfilesDescription',
};
export const PREMIUM_FEATURE_SECTIONS = [
'double_limits',
'more_upload',
'faster_download',
'voice_to_text',
'no_ads',
'unique_reactions',
'premium_stickers',
'animated_emoji',
'advanced_chat_management',
'profile_badge',
'animated_userpics',
];
const PREMIUM_BOTTOM_VIDEOS: string[] = [
'faster_download',
'voice_to_text',
'advanced_chat_management',
'profile_badge',
'animated_userpics',
];
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts'>;
const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
'channels',
'dialogFolderPinned',
'channelsPublic',
'savedGifs',
'stickersFaved',
'aboutLength',
'captionLength',
'dialogFilters',
'dialogFiltersChats',
];
const LIMITS_TITLES: Record<ApiLimitTypeWithoutUpload, string> = {
channels: 'GroupsAndChannelsLimitTitle',
dialogFolderPinned: 'PinChatsLimitTitle',
channelsPublic: 'PublicLinksLimitTitle',
savedGifs: 'SavedGifsLimitTitle',
stickersFaved: 'FavoriteStickersLimitTitle',
aboutLength: 'BioLimitTitle',
captionLength: 'CaptionsLimitTitle',
dialogFilters: 'FoldersLimitTitle',
dialogFiltersChats: 'ChatPerFolderLimitTitle',
};
const LIMITS_DESCRIPTIONS: Record<ApiLimitTypeWithoutUpload, string> = {
channels: 'GroupsAndChannelsLimitSubtitle',
dialogFolderPinned: 'PinChatsLimitSubtitle',
channelsPublic: 'PublicLinksLimitSubtitle',
savedGifs: 'SavedGifsLimitSubtitle',
stickersFaved: 'FavoriteStickersLimitSubtitle',
aboutLength: 'BioLimitSubtitle',
captionLength: 'CaptionsLimitSubtitle',
dialogFilters: 'FoldersLimitSubtitle',
dialogFiltersChats: 'ChatPerFolderLimitSubtitle',
};
const BORDER_THRESHOLD = 20;
type OwnProps = {
onBack: VoidFunction;
initialSection: string;
promo: ApiPremiumPromo;
onClickSubscribe: (startParam?: string) => void;
isPremium?: boolean;
limits?: NonNullable<GlobalState['appConfig']>['limits'];
};
const PremiumFeatureModal: FC<OwnProps> = ({
promo,
initialSection,
onBack,
onClickSubscribe,
isPremium,
limits,
}) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [currentSlideIndex, setCurrentSlideIndex] = useState(PREMIUM_FEATURE_SECTIONS.indexOf(initialSection));
const [reverseAnimationSlideIndex, setReverseAnimationSlideIndex] = useState(0);
const [isScrolling, startScrolling, stopScrolling] = useFlag();
const [isScrolledToTop, setIsScrolledToTop] = useState(true);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);
const prevInitialSection = usePrevious(initialSection);
function handleClick() {
onClickSubscribe(initialSection);
}
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
const { clientWidth, scrollLeft: scrollLeftOriginal } = e.currentTarget;
const scrollLeft = Math.round(scrollLeftOriginal);
const left = scrollLeft % (clientWidth);
const progress = left / (clientWidth);
e.currentTarget.style.setProperty('--scroll-progress', progress.toString());
e.currentTarget.style.setProperty('--abs-scroll-progress', Math.abs(progress).toString());
const reverseIndex = Math.ceil((scrollLeft + 1) / clientWidth);
setReverseAnimationSlideIndex(reverseIndex);
const prevElement = e.currentTarget.querySelector(`#premium_feature_preview_video_${reverseIndex - 1}`);
const reverseElement = e.currentTarget.querySelector(`#premium_feature_preview_video_${reverseIndex}`);
prevElement?.classList.toggle('reverse', false);
reverseElement?.classList.toggle('reverse', true);
if (isScrolling) return;
const slide = Math.round(scrollLeft / clientWidth);
setCurrentSlideIndex(slide);
}
function handleLimitsScroll(e: React.UIEvent<HTMLDivElement>) {
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget;
setIsScrolledToTop(scrollTop <= BORDER_THRESHOLD);
setIsScrolledToBottom(scrollTop >= scrollHeight - clientHeight - BORDER_THRESHOLD);
}
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer || (prevInitialSection === initialSection)) return;
const index = PREMIUM_FEATURE_SECTIONS.indexOf(initialSection);
setCurrentSlideIndex(index);
startScrolling();
fastSmoothScrollHorizontal(scrollContainer, scrollContainer.clientWidth * index, 0)
.then(stopScrolling);
}, [currentSlideIndex, initialSection, prevInitialSection, startScrolling, stopScrolling]);
const handleSelectSlide = useCallback(async (index: number) => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
setCurrentSlideIndex(index);
startScrolling();
await fastSmoothScrollHorizontal(scrollContainer, scrollContainer.clientWidth * index, 800);
stopScrolling();
}, [startScrolling, stopScrolling]);
return (
<div className={styles.root}>
<Button
round
size="smaller"
className={buildClassName(styles.backButton, currentSlideIndex !== 0 && styles.whiteBackButton)}
color={currentSlideIndex === 0 ? 'translucent' : 'translucent-white'}
onClick={onBack}
ariaLabel={lang('Back')}
>
<i className="icon-arrow-left" />
</Button>
<div className={styles.preview} />
<div className={buildClassName(styles.content, 'no-scrollbar')} onScroll={handleScroll} ref={scrollContainerRef}>
{PREMIUM_FEATURE_SECTIONS.map((section, index) => {
if (section === 'double_limits') {
return (
<div className={buildClassName(styles.slide, styles.limits)}>
<h2 className={buildClassName(styles.header, isScrolledToTop && styles.noHeaderBorder)}>
{lang(PREMIUM_FEATURE_TITLES.double_limits)}
</h2>
<div className={buildClassName(styles.limitsContent, 'custom-scroll')} onScroll={handleLimitsScroll}>
{LIMITS_ORDER.map((limit, i) => {
const defaultLimit = limits?.[limit][0].toString();
const premiumLimit = limits?.[limit][1].toString();
return (
<PremiumLimitPreview
title={lang(LIMITS_TITLES[limit])}
description={lang(LIMITS_DESCRIPTIONS[limit], premiumLimit)}
leftValue={defaultLimit}
rightValue={premiumLimit}
colorStepProgress={i / (LIMITS_ORDER.length - 1)}
/>
);
})}
</div>
</div>
);
}
if (section === 'unique_reactions') {
return (
<div className={styles.slide}>
<div className={styles.frame}>
<PremiumFeaturePreviewReactions isActive={currentSlideIndex === index} />
</div>
<h1 className={styles.title}>
{lang(PREMIUM_FEATURE_TITLES.unique_reactions)}
</h1>
<div className={styles.description}>
{renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS.unique_reactions), ['br'])}
</div>
</div>
);
}
if (section === 'premium_stickers') {
return (
<div className={styles.slide}>
<div className={styles.frame}>
<PremiumFeaturePreviewStickers isActive={currentSlideIndex === index} />
</div>
<h1 className={styles.title}>
{lang(PREMIUM_FEATURE_TITLES.premium_stickers)}
</h1>
<div className={styles.description}>
{renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS.premium_stickers), ['br'])}
</div>
</div>
);
}
const i = promo.videoSections.indexOf(section);
if (i === -1) return undefined;
return (
<div className={styles.slide}>
<div className={styles.frame}>
<PremiumFeaturePreviewVideo
isActive={currentSlideIndex === index}
videoId={promo.videos[i].id!}
videoThumbnail={promo.videos[i].thumbnail!}
isDown={PREMIUM_BOTTOM_VIDEOS.includes(section)}
index={index}
isReverseAnimation={index === reverseAnimationSlideIndex}
/>
</div>
<h1 className={styles.title}>
{lang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]!])}
</h1>
<div className={styles.description}>
{renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]!]), ['br'])}
</div>
</div>
);
})}
</div>
<div
className={buildClassName(
styles.footer,
(isScrolledToBottom || currentSlideIndex !== 0) && styles.noFooterBorder,
)}
>
<SliderDots
length={PREMIUM_FEATURE_SECTIONS.length}
active={currentSlideIndex}
onSelectSlide={handleSelectSlide}
/>
<Button
className={buildClassName(styles.button, !isPremium && styles.buttonPremium)}
isShiny={!isPremium}
onClick={isPremium ? onBack : handleClick}
>
{isPremium
? lang('OK')
: lang('SubscribeToPremium', formatCurrency(Number(promo.monthlyAmount), promo.currency, lang.code))}
</Button>
</div>
</div>
);
};
export default memo(PremiumFeatureModal);