326 lines
12 KiB
TypeScript
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);
|