542 lines
18 KiB
TypeScript
542 lines
18 KiB
TypeScript
import type { FC } from '@teact';
|
|
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import type {
|
|
ApiPremiumPromo,
|
|
ApiPremiumSection,
|
|
ApiPremiumSubscriptionOption,
|
|
ApiStarGift,
|
|
ApiSticker,
|
|
ApiStickerSet,
|
|
ApiUser,
|
|
} from '../../../api/types';
|
|
import type { GlobalState } from '../../../global/types';
|
|
import type { LangPair } from '../../../types/language';
|
|
|
|
import { PREMIUM_FEATURE_SECTIONS, TME_LINK_PREFIX } from '../../../config';
|
|
import { getUserFullName } from '../../../global/helpers';
|
|
import {
|
|
selectCustomEmoji,
|
|
selectIsCurrentUserPremium,
|
|
selectStickerSet,
|
|
selectTabState,
|
|
selectUser,
|
|
} from '../../../global/selectors';
|
|
import { selectPremiumLimit } from '../../../global/selectors/limits';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { formatCountdownDays } from '../../../util/dates/oldDateFormat';
|
|
import { formatCurrency } from '../../../util/formatCurrency';
|
|
import { getStickerFromGift } from '../../common/helpers/gifts';
|
|
import { REM } from '../../common/helpers/mediaDimensions';
|
|
import renderText from '../../common/helpers/renderText';
|
|
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
|
|
|
import useLang from '../../../hooks/useLang';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
import useSyncEffect from '../../../hooks/useSyncEffect';
|
|
|
|
import CustomEmoji from '../../common/CustomEmoji';
|
|
import ParticlesHeader from '../../modals/common/ParticlesHeader.tsx';
|
|
import Button from '../../ui/Button';
|
|
import Modal from '../../ui/Modal';
|
|
import Transition from '../../ui/Transition';
|
|
import PremiumFeatureItem from './PremiumFeatureItem';
|
|
import PremiumFeatureModal, { PREMIUM_FEATURE_DESCRIPTIONS, PREMIUM_FEATURE_TITLES } from './PremiumFeatureModal';
|
|
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
|
|
|
|
import styles from './PremiumMainModal.module.scss';
|
|
|
|
import PremiumAds from '../../../assets/premium/PremiumAds.svg';
|
|
import PremiumAi from '../../../assets/premium/PremiumAi.svg';
|
|
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
|
import PremiumChats from '../../../assets/premium/PremiumChats.svg';
|
|
import PremiumEffects from '../../../assets/premium/PremiumEffects.svg';
|
|
import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg';
|
|
import PremiumFile from '../../../assets/premium/PremiumFile.svg';
|
|
import PremiumLastSeen from '../../../assets/premium/PremiumLastSeen.svg';
|
|
import PremiumLimits from '../../../assets/premium/PremiumLimits.svg';
|
|
import PremiumMessagePrivacy from '../../../assets/premium/PremiumMessagePrivacy.svg';
|
|
import PremiumNoforwards from '../../../assets/premium/PremiumNoForwardsPrivacy.svg';
|
|
import PremiumReactions from '../../../assets/premium/PremiumReactions.svg';
|
|
import PremiumSpeed from '../../../assets/premium/PremiumSpeed.svg';
|
|
import PremiumStatus from '../../../assets/premium/PremiumStatus.svg';
|
|
import PremiumStickers from '../../../assets/premium/PremiumStickers.svg';
|
|
import PremiumTags from '../../../assets/premium/PremiumTags.svg';
|
|
import PremiumTranslate from '../../../assets/premium/PremiumTranslate.svg';
|
|
import PremiumVideo from '../../../assets/premium/PremiumVideo.svg';
|
|
import PremiumVoice from '../../../assets/premium/PremiumVoice.svg';
|
|
|
|
const LIMIT_ACCOUNTS = 4;
|
|
const STATUS_EMOJI_SIZE = 8 * REM;
|
|
|
|
const PREMIUM_FEATURE_COLOR_ICONS: Record<ApiPremiumSection, string> = {
|
|
stories: PremiumStatus,
|
|
double_limits: PremiumLimits,
|
|
infinite_reactions: PremiumReactions,
|
|
premium_stickers: PremiumStickers,
|
|
animated_emoji: PremiumEmoji,
|
|
no_ads: PremiumAds,
|
|
voice_to_text: PremiumVoice,
|
|
profile_badge: PremiumBadge,
|
|
faster_download: PremiumSpeed,
|
|
more_upload: PremiumFile,
|
|
advanced_chat_management: PremiumChats,
|
|
animated_userpics: PremiumVideo,
|
|
emoji_status: PremiumStatus,
|
|
translations: PremiumTranslate,
|
|
saved_tags: PremiumTags,
|
|
last_seen: PremiumLastSeen,
|
|
message_privacy: PremiumMessagePrivacy,
|
|
effects: PremiumEffects,
|
|
ai_compose: PremiumAi,
|
|
todo: PremiumBadge,
|
|
pm_noforwards: PremiumNoforwards,
|
|
};
|
|
|
|
export type OwnProps = {
|
|
isOpen?: boolean;
|
|
};
|
|
|
|
type StateProps = {
|
|
currentUserId?: string;
|
|
promo?: ApiPremiumPromo;
|
|
fromUser?: ApiUser;
|
|
fromUserStatusEmoji?: ApiSticker;
|
|
fromUserStatusSet?: ApiStickerSet;
|
|
toUser?: ApiUser;
|
|
initialSection?: ApiPremiumSection;
|
|
isPremium?: boolean;
|
|
isSuccess?: boolean;
|
|
isGift?: boolean;
|
|
daysAmount?: number;
|
|
gift?: ApiStarGift;
|
|
limitChannels: number;
|
|
limitPins: number;
|
|
limitLinks: number;
|
|
limitFolders: number;
|
|
limits?: NonNullable<GlobalState['appConfig']>['limits'];
|
|
premiumSlug?: string;
|
|
premiumBotUsername?: string;
|
|
premiumPromoOrder?: ApiPremiumSection[];
|
|
};
|
|
|
|
const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
|
isOpen,
|
|
currentUserId,
|
|
fromUser,
|
|
fromUserStatusEmoji,
|
|
fromUserStatusSet,
|
|
promo,
|
|
initialSection,
|
|
isPremium,
|
|
limitChannels,
|
|
limitLinks,
|
|
limitFolders,
|
|
limitPins,
|
|
limits,
|
|
premiumSlug,
|
|
premiumBotUsername,
|
|
isSuccess,
|
|
isGift,
|
|
toUser,
|
|
daysAmount,
|
|
premiumPromoOrder,
|
|
gift,
|
|
}) => {
|
|
const dialogRef = useRef<HTMLDivElement>();
|
|
const {
|
|
closePremiumModal, openInvoice, requestConfetti, openTelegramLink, loadStickers, openStickerSet,
|
|
} = getActions();
|
|
|
|
const oldLang = useOldLang();
|
|
const lang = useLang();
|
|
const [isHeaderHidden, setIsHeaderHidden] = useState(true);
|
|
const [currentSection, setCurrentSection] = useState<ApiPremiumSection | undefined>(initialSection);
|
|
const [selectedSubscriptionOption, setSelectedSubscriptionOption] = useState<ApiPremiumSubscriptionOption>();
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setIsHeaderHidden(true);
|
|
setCurrentSection(undefined);
|
|
} else if (initialSection) {
|
|
setCurrentSection(initialSection);
|
|
}
|
|
}, [isOpen, initialSection]);
|
|
|
|
const handleOpenSection = useLastCallback((section: ApiPremiumSection) => {
|
|
setCurrentSection(section);
|
|
});
|
|
|
|
const handleResetSection = useLastCallback(() => {
|
|
setCurrentSection(undefined);
|
|
});
|
|
|
|
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
|
|
const { scrollTop } = e.currentTarget;
|
|
|
|
setIsHeaderHidden(scrollTop <= 150);
|
|
}
|
|
|
|
const handleClickWithStartParam = useLastCallback((startParam?: string) => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog) return;
|
|
|
|
if (premiumSlug) {
|
|
openInvoice({
|
|
type: 'slug',
|
|
slug: premiumSlug,
|
|
});
|
|
} else if (premiumBotUsername) {
|
|
openTelegramLink({
|
|
url: `${TME_LINK_PREFIX}${premiumBotUsername}?start=${startParam || 'promo'}`,
|
|
});
|
|
closePremiumModal();
|
|
}
|
|
});
|
|
|
|
const handleClick = useLastCallback(() => {
|
|
if (selectedSubscriptionOption) {
|
|
handleClickWithStartParam(String(selectedSubscriptionOption.months));
|
|
} else {
|
|
handleClickWithStartParam();
|
|
}
|
|
});
|
|
|
|
const handleChangeSubscriptionOption = useLastCallback((months: number) => {
|
|
const foundOption = promo?.options.find((option) => option.months === months);
|
|
setSelectedSubscriptionOption(foundOption);
|
|
});
|
|
|
|
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 (isSuccess) {
|
|
showConfetti();
|
|
}
|
|
}, [isSuccess, showConfetti]);
|
|
|
|
useSyncEffect(([prevIsPremium]) => {
|
|
if (prevIsPremium === isPremium) return;
|
|
|
|
showConfetti();
|
|
}, [isPremium, showConfetti]);
|
|
|
|
const filteredSections = useMemo(() => {
|
|
if (!premiumPromoOrder) return PREMIUM_FEATURE_SECTIONS;
|
|
return premiumPromoOrder.filter((section) => PREMIUM_FEATURE_SECTIONS.includes(section));
|
|
}, [premiumPromoOrder]);
|
|
|
|
useEffect(() => {
|
|
if (!fromUserStatusEmoji || fromUserStatusSet) return;
|
|
loadStickers({
|
|
stickerSetInfo: fromUserStatusEmoji.stickerSetInfo,
|
|
});
|
|
}, [loadStickers, fromUserStatusEmoji, fromUserStatusSet]);
|
|
|
|
useEffect(() => {
|
|
const [defaultOption] = promo?.options ?? [];
|
|
setSelectedSubscriptionOption(defaultOption);
|
|
}, [promo]);
|
|
|
|
const handleOpenStatusSet = useLastCallback(() => {
|
|
if (!fromUserStatusSet) return;
|
|
|
|
openStickerSet({
|
|
stickerSetInfo: fromUserStatusSet,
|
|
});
|
|
});
|
|
|
|
const fullMonthlyAmount = useMemo(() => {
|
|
const monthOption = promo?.options.find((option) => option.months === 1);
|
|
if (!monthOption) {
|
|
return undefined;
|
|
}
|
|
return Number(monthOption.amount);
|
|
}, [promo]);
|
|
|
|
const subscribeButtonText = useMemo(() => {
|
|
if (!selectedSubscriptionOption) {
|
|
return undefined;
|
|
}
|
|
const { amount, months, currency } = selectedSubscriptionOption;
|
|
const perMonthPrice = Math.floor(amount / months);
|
|
return formatCurrency(
|
|
lang,
|
|
perMonthPrice,
|
|
currency,
|
|
);
|
|
}, [selectedSubscriptionOption, lang]);
|
|
|
|
if (!promo || (fromUserStatusEmoji && !fromUserStatusSet)) return undefined;
|
|
|
|
function getHeaderText() {
|
|
if (gift) {
|
|
return lang('PremiumGiftHeader');
|
|
}
|
|
|
|
if (isGift) {
|
|
const formattedDuration = daysAmount ? formatCountdownDays(lang, daysAmount) : '';
|
|
return renderText(
|
|
fromUser?.id === currentUserId
|
|
? lang('DialogTitlePremiumGiftSentTo', { user: getUserFullName(toUser), amount: formattedDuration })
|
|
: lang('DialogTitlePremiumGiftReceivedFrom', { user: getUserFullName(fromUser), amount: formattedDuration }),
|
|
['simple_markdown', 'emoji'],
|
|
);
|
|
}
|
|
|
|
if (fromUserStatusSet && fromUser) {
|
|
const template = oldLang('lng_premium_emoji_status_title').replace('{user}', getUserFullName(fromUser)!);
|
|
const [first, second] = template.split('{link}');
|
|
|
|
const emoji = fromUserStatusSet.thumbCustomEmojiId ? (
|
|
<CustomEmoji className={styles.stickerSetLinkIcon} documentId={fromUserStatusSet.thumbCustomEmojiId} />
|
|
) : undefined;
|
|
const link = (
|
|
<span className={styles.stickerSetLink} onClick={handleOpenStatusSet}>
|
|
{emoji}
|
|
{renderText(fromUserStatusSet.title)}
|
|
</span>
|
|
);
|
|
return [renderText(first), link, renderText(second)];
|
|
}
|
|
|
|
return renderText(
|
|
fromUser
|
|
? oldLang('TelegramPremiumUserDialogTitle', getUserFullName(fromUser))
|
|
: oldLang(isPremium ? 'TelegramPremiumSubscribedTitle' : 'TelegramPremium'),
|
|
['simple_markdown', 'emoji'],
|
|
);
|
|
}
|
|
|
|
function getHeaderDescription() {
|
|
if (gift && gift.type !== 'starGiftUnique' && gift.perUserTotal) {
|
|
return lang('DescriptionGiftPremiumRequired2', { count: gift.perUserTotal }, {
|
|
pluralValue: gift.perUserTotal,
|
|
});
|
|
}
|
|
|
|
if (isGift) {
|
|
return fromUser?.id === currentUserId
|
|
? oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', getUserFullName(toUser))
|
|
: oldLang('TelegramPremiumUserGiftedPremiumDialogSubtitle');
|
|
}
|
|
|
|
if (fromUserStatusSet) {
|
|
return oldLang('TelegramPremiumUserStatusDialogSubtitle');
|
|
}
|
|
|
|
return fromUser
|
|
? oldLang('TelegramPremiumUserDialogSubtitle')
|
|
: oldLang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle');
|
|
}
|
|
|
|
function renderHeader() {
|
|
if (gift) {
|
|
const giftSticker = getStickerFromGift(gift);
|
|
return (
|
|
<ParticlesHeader
|
|
model="sticker"
|
|
sticker={giftSticker}
|
|
color="purple"
|
|
title={getHeaderText()}
|
|
description={renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
|
|
className={styles.giftParticlesHeader}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (!fromUserStatusEmoji) {
|
|
return (
|
|
<ParticlesHeader
|
|
model="swaying-star"
|
|
color="purple"
|
|
title={getHeaderText()}
|
|
description={renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
|
|
className={styles.starParticlesHeader}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<CustomEmoji
|
|
className={styles.statusEmoji}
|
|
onClick={handleOpenStatusSet}
|
|
documentId={fromUserStatusEmoji.id}
|
|
isBig
|
|
size={STATUS_EMOJI_SIZE}
|
|
/>
|
|
<h2 className={buildClassName(styles.headerText, fromUserStatusSet && styles.stickerSetText)}>
|
|
{getHeaderText()}
|
|
</h2>
|
|
<div className={styles.description}>
|
|
{renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function renderFooterText() {
|
|
if (!promo || (isGift && fromUser?.id === currentUserId)) {
|
|
return undefined;
|
|
}
|
|
|
|
return (
|
|
<div className={styles.footerText} dir={lang.isRtl ? 'rtl' : undefined}>
|
|
{renderTextWithEntities({
|
|
text: promo.statusText,
|
|
entities: promo.statusEntities,
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function renderSubscriptionOptions() {
|
|
return (
|
|
<div className={styles.subscriptionOptions}>
|
|
{promo?.options
|
|
.map((option) => (
|
|
<PremiumSubscriptionOption
|
|
className={styles.subscriptionOption}
|
|
key={option.amount}
|
|
option={option}
|
|
onChange={handleChangeSubscriptionOption}
|
|
fullMonthlyAmount={fullMonthlyAmount}
|
|
checked={selectedSubscriptionOption?.months === option.months}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
className={styles.root}
|
|
onClose={closePremiumModal}
|
|
isOpen={isOpen}
|
|
dialogRef={dialogRef}
|
|
hasAbsoluteCloseButton={!currentSection}
|
|
>
|
|
<Transition name="slide" activeKey={currentSection ? 1 : 0} className={styles.transition}>
|
|
{!currentSection ? (
|
|
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
|
|
{renderHeader()}
|
|
{!isPremium && !isGift && renderSubscriptionOptions()}
|
|
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
|
|
<h2 className={styles.premiumHeaderText}>
|
|
{oldLang('TelegramPremium')}
|
|
</h2>
|
|
</div>
|
|
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
|
|
{filteredSections.map((section, index) => {
|
|
const shouldUseNewLang = section === 'todo' || section === 'pm_noforwards' || section === 'ai_compose';
|
|
return (
|
|
<PremiumFeatureItem
|
|
key={section}
|
|
title={shouldUseNewLang
|
|
? lang(PREMIUM_FEATURE_TITLES[section] as keyof LangPair)
|
|
: oldLang(PREMIUM_FEATURE_TITLES[section])}
|
|
text={section === 'double_limits'
|
|
? oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section],
|
|
[limitChannels, limitFolders, limitPins, limitLinks, LIMIT_ACCOUNTS])
|
|
: shouldUseNewLang
|
|
? lang(PREMIUM_FEATURE_DESCRIPTIONS[section] as keyof LangPair)
|
|
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section])}
|
|
icon={PREMIUM_FEATURE_COLOR_ICONS[section]}
|
|
index={index}
|
|
count={filteredSections.length}
|
|
section={section}
|
|
onClick={handleOpenSection}
|
|
/>
|
|
);
|
|
})}
|
|
<div
|
|
className={buildClassName(styles.footerText, styles.primaryFooterText)}
|
|
dir={lang.isRtl ? 'rtl' : undefined}
|
|
>
|
|
<p>
|
|
{renderText(oldLang('AboutPremiumDescription'), ['simple_markdown'])}
|
|
</p>
|
|
<p>
|
|
{renderText(oldLang('AboutPremiumDescription2'), ['simple_markdown'])}
|
|
</p>
|
|
</div>
|
|
{renderFooterText()}
|
|
</div>
|
|
{!isPremium && selectedSubscriptionOption && (
|
|
<div className={styles.footer}>
|
|
<Button className={styles.button} isShiny withPremiumGradient onClick={handleClick}>
|
|
{oldLang('SubscribeToPremium', subscribeButtonText)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<PremiumFeatureModal
|
|
initialSection={currentSection}
|
|
onBack={handleResetSection}
|
|
promo={promo}
|
|
onClickSubscribe={handleClickWithStartParam}
|
|
isPremium={isPremium}
|
|
limits={limits}
|
|
premiumPromoOrder={premiumPromoOrder}
|
|
subscriptionOption={selectedSubscriptionOption}
|
|
/>
|
|
)}
|
|
</Transition>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>((global): Complete<StateProps> => {
|
|
const {
|
|
premiumModal,
|
|
} = selectTabState(global);
|
|
|
|
const fromUser = premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined;
|
|
const fromUserStatusEmoji = fromUser?.emojiStatus ? selectCustomEmoji(global, fromUser.emojiStatus.documentId)
|
|
: undefined;
|
|
const fromUserStatusSet = fromUserStatusEmoji ? selectStickerSet(global, fromUserStatusEmoji.stickerSetInfo)
|
|
: undefined;
|
|
|
|
return {
|
|
currentUserId: global.currentUserId,
|
|
promo: premiumModal?.promo,
|
|
isSuccess: premiumModal?.isSuccess,
|
|
isGift: premiumModal?.isGift,
|
|
daysAmount: premiumModal?.daysAmount,
|
|
gift: premiumModal?.gift,
|
|
fromUser,
|
|
fromUserStatusEmoji,
|
|
fromUserStatusSet,
|
|
toUser: premiumModal?.toUserId ? selectUser(global, premiumModal.toUserId) : undefined,
|
|
initialSection: premiumModal?.initialSection,
|
|
isPremium: selectIsCurrentUserPremium(global),
|
|
limitChannels: selectPremiumLimit(global, 'channels'),
|
|
limitFolders: selectPremiumLimit(global, 'dialogFilters'),
|
|
limitPins: selectPremiumLimit(global, 'dialogFolderPinned'),
|
|
limitLinks: selectPremiumLimit(global, 'channelsPublic'),
|
|
limits: global.appConfig.limits,
|
|
premiumSlug: global.appConfig.premiumInvoiceSlug,
|
|
premiumBotUsername: global.appConfig.premiumBotUsername,
|
|
premiumPromoOrder: global.appConfig.premiumPromoOrder,
|
|
};
|
|
})(PremiumMainModal));
|