Gift Auction Modal: Add upgrade preview in header (#6573)
This commit is contained in:
parent
9308631302
commit
bcb276d568
@ -63,7 +63,8 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
||||
const {
|
||||
id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut,
|
||||
birthday, upgradeStars, resellMinStars, title, availabilityResale, releasedBy,
|
||||
requirePremium, limitedPerUser, perUserTotal, perUserRemains, lockedUntilDate, auction, giftsPerRound, background,
|
||||
requirePremium, limitedPerUser, perUserTotal, perUserRemains, lockedUntilDate, auction, auctionSlug, giftsPerRound,
|
||||
background,
|
||||
} = starGift;
|
||||
|
||||
addDocumentToLocalDb(starGift.sticker);
|
||||
@ -94,6 +95,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
||||
perUserRemains,
|
||||
lockedUntilDate,
|
||||
isAuction: auction,
|
||||
auctionSlug,
|
||||
giftsPerRound,
|
||||
background: background ? {
|
||||
centerColor: int2hex(background.centerColor),
|
||||
|
||||
@ -119,8 +119,11 @@ export type ApiReceipt = ApiReceiptRegular | ApiReceiptStars;
|
||||
|
||||
export type ApiPremiumSection = typeof PREMIUM_FEATURE_SECTIONS[number];
|
||||
|
||||
// Video sections can include additional values like 'gifts' that are not premium features
|
||||
export type ApiPromoVideoSection = ApiPremiumSection | 'gifts';
|
||||
|
||||
export interface ApiPremiumPromo {
|
||||
videoSections: ApiPremiumSection[];
|
||||
videoSections: ApiPromoVideoSection[];
|
||||
videos: ApiDocument[];
|
||||
statusText: string;
|
||||
statusEntities: ApiMessageEntity[];
|
||||
|
||||
@ -28,6 +28,7 @@ export interface ApiStarGiftRegular {
|
||||
perUserRemains?: number;
|
||||
lockedUntilDate?: number;
|
||||
isAuction?: true;
|
||||
auctionSlug?: string;
|
||||
giftsPerRound?: number;
|
||||
background?: ApiStarGiftBackground;
|
||||
}
|
||||
|
||||
@ -2342,6 +2342,8 @@
|
||||
"ContextMenuHintTouch" = "To edit or reply, close this menu. Then long tap on a message.";
|
||||
"GiftValueForSaleOnFragment" = "for sale on Fragment";
|
||||
"GiftValueForSaleOnTelegram" = "for sale on Telegram";
|
||||
"GiftAuctionForSaleOnFragment" = "{count} for sale on Fragment >";
|
||||
"GiftAuctionForSaleOnTelegram" = "{count} for sale on Telegram >";
|
||||
"EmbeddedMessageNoCaption" = "Caption removed";
|
||||
"ConfirmBuyGiftForTonDescription" = "The seller only accepts TON as payment.";
|
||||
"TitleGiftLocked" = "Gift Locked";
|
||||
@ -2478,6 +2480,16 @@
|
||||
"GiftAuctionAveragePrice" = "Average Price";
|
||||
"GiftAuctionTapToBidMore" = "click to bid more";
|
||||
"GiftAuctionWonNotification" = "You won {gift} at the auction!";
|
||||
"GiftAuctionLearnMoreAboutGifts" = "Learn more about Telegram Gifts >";
|
||||
"GiftAuctionLearnMoreMenuItem" = "Learn More";
|
||||
"StarGiftInfoTitle" = "Telegram Gifts";
|
||||
"StarGiftInfoSubtitle" = "Gifts are collectible items you can trade or showcase on your profile.";
|
||||
"StarGiftInfoUniqueTitle" = "Unique";
|
||||
"StarGiftInfoUniqueSubtitle" = "Upgrade your gifts to get a unique number, model, backdrop and symbol.";
|
||||
"StarGiftInfoTradableTitle" = "Tradable";
|
||||
"StarGiftInfoTradableSubtitle" = "Sell your gift on Telegram or on third-party NFT marketplaces.";
|
||||
"StarGiftInfoWearableTitle" = "Wearable";
|
||||
"StarGiftInfoWearableSubtitle" = "Display gifts on your profile and set them as your cover or status.";
|
||||
"StarGift" = "Star Gift";
|
||||
"SettingsItemPrivacyPasskeys" = "Passkeys";
|
||||
"SettingsItemPrivacyOn" = "Enabled";
|
||||
|
||||
@ -20,6 +20,7 @@ export { default as GiftAuctionInfoModal } from '../components/modals/gift/aucti
|
||||
export { default as GiftAuctionAcquiredModal } from '../components/modals/gift/auction/GiftAuctionAcquiredModal';
|
||||
export { default as GiftAuctionChangeRecipientModal } from '../components/modals/gift/auction/GiftAuctionChangeRecipientModal';
|
||||
export { default as StarGiftPriceDecreaseInfoModal } from '../components/modals/gift/StarGiftPriceDecreaseInfoModal';
|
||||
export { default as AboutStarGiftModal } from '../components/modals/gift/AboutStarGiftModal';
|
||||
export { default as GiftStatusInfoModal } from '../components/modals/gift/status/GiftStatusInfoModal';
|
||||
export { default as GiftWithdrawModal } from '../components/modals/gift/withdraw/GiftWithdrawModal';
|
||||
export { default as GiftTransferModal } from '../components/modals/gift/transfer/GiftTransferModal';
|
||||
|
||||
@ -7,6 +7,10 @@ import type {
|
||||
ApiStarGiftAttributePattern,
|
||||
ApiSticker,
|
||||
} from '../../../api/types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { getStickerMediaHash } from '../../../global/helpers';
|
||||
import { fetch } from '../../../util/mediaLoader';
|
||||
|
||||
export type GiftAttributes = {
|
||||
model?: ApiStarGiftAttributeModel;
|
||||
@ -101,3 +105,17 @@ export function getRandomGiftPreviewAttributes(
|
||||
backdrop: randomBackdrop,
|
||||
};
|
||||
}
|
||||
|
||||
export function preloadGiftAttributeStickers(attributes: ApiStarGiftAttribute[]) {
|
||||
const patternStickers = attributes
|
||||
.filter((attr): attr is ApiStarGiftAttributePattern => attr.type === 'pattern')
|
||||
.map((attr) => attr.sticker);
|
||||
const modelStickers = attributes
|
||||
.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model')
|
||||
.map((attr) => attr.sticker);
|
||||
|
||||
const mediaHashes = [...patternStickers, ...modelStickers].map((sticker) => getStickerMediaHash(sticker, 'full'));
|
||||
mediaHashes.forEach((hash) => {
|
||||
fetch(hash, ApiMediaFormat.BlobUrl);
|
||||
});
|
||||
}
|
||||
|
||||
@ -291,14 +291,14 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
|
||||
const i = promo.videoSections.indexOf(section);
|
||||
if (i === -1) return undefined;
|
||||
const shouldUseNewLang = promo.videoSections[i] === 'todo';
|
||||
const shouldUseNewLang = section === 'todo';
|
||||
return (
|
||||
<div className={styles.slide}>
|
||||
<div className={styles.frame}>
|
||||
<PremiumFeaturePreviewVideo
|
||||
isActive={currentSlideIndex === index}
|
||||
videoId={promo.videos[i].id!}
|
||||
videoThumbnail={promo.videos[i].thumbnail!}
|
||||
videoId={promo.videos[i].id}
|
||||
videoThumbnail={promo.videos[i].thumbnail}
|
||||
isDown={PREMIUM_BOTTOM_VIDEOS.includes(section)}
|
||||
index={index}
|
||||
isReverseAnimation={index === reverseAnimationSlideIndex}
|
||||
@ -307,20 +307,20 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
<h1 className={styles.title}>
|
||||
{shouldUseNewLang
|
||||
? lang(
|
||||
PREMIUM_FEATURE_TITLES[promo.videoSections[i]] as keyof LangPair,
|
||||
PREMIUM_FEATURE_TITLES['todo'] as keyof LangPair,
|
||||
undefined,
|
||||
{ withNodes: true, renderTextFilters: ['br'] },
|
||||
)
|
||||
: oldLang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]])}
|
||||
: oldLang(PREMIUM_FEATURE_TITLES[section])}
|
||||
</h1>
|
||||
<div className={styles.description}>
|
||||
{renderText(shouldUseNewLang
|
||||
? lang(
|
||||
PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]] as keyof LangPair,
|
||||
PREMIUM_FEATURE_DESCRIPTIONS['todo'] as keyof LangPair,
|
||||
undefined,
|
||||
{ withNodes: true, renderTextFilters: ['br'] },
|
||||
)
|
||||
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]]), ['br'],
|
||||
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section]), ['br'],
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.down {
|
||||
--y-static: 3%;
|
||||
--y-static: 2%;
|
||||
--y-dynamic: 10%;
|
||||
}
|
||||
|
||||
@ -67,3 +67,16 @@
|
||||
bottom: initial;
|
||||
border-radius: 10% 10% 0 0;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
--placeholder-background: #555555;
|
||||
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
|
||||
width: 97%;
|
||||
height: 97%;
|
||||
border-radius: 10%;
|
||||
|
||||
background-color: var(--placeholder-background);
|
||||
}
|
||||
|
||||
@ -16,47 +16,55 @@ import styles from './PremiumFeaturePreviewVideo.module.scss';
|
||||
import DeviceFrame from '../../../../assets/premium/DeviceFrame.svg';
|
||||
|
||||
type OwnProps = {
|
||||
videoId: string;
|
||||
isReverseAnimation: boolean;
|
||||
isDown: boolean;
|
||||
videoThumbnail: ApiThumbnail;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
videoId?: string;
|
||||
videoThumbnail?: ApiThumbnail;
|
||||
isActive?: boolean;
|
||||
isReverseAnimation?: boolean;
|
||||
isDown?: boolean;
|
||||
index?: number;
|
||||
className?: string;
|
||||
wrapperClassName?: string;
|
||||
};
|
||||
|
||||
const PremiumFeaturePreviewVideo: FC<OwnProps> = ({
|
||||
videoId,
|
||||
videoThumbnail,
|
||||
isActive,
|
||||
isReverseAnimation,
|
||||
isDown,
|
||||
videoThumbnail,
|
||||
index,
|
||||
isActive,
|
||||
className,
|
||||
wrapperClassName,
|
||||
}) => {
|
||||
const mediaData = useMedia(`document${videoId}`);
|
||||
const thumbnailRef = useCanvasBlur(videoThumbnail.dataUri);
|
||||
const mediaData = useMedia(videoId ? `document${videoId}` : undefined);
|
||||
const thumbnailRef = useCanvasBlur(videoThumbnail?.dataUri);
|
||||
const transitionClassNames = useMediaTransitionDeprecated(mediaData);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={buildClassName(styles.root, className)}>
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.wrapper,
|
||||
isReverseAnimation && styles.reverse,
|
||||
isDown && styles.down,
|
||||
wrapperClassName,
|
||||
)}
|
||||
id={`premium_feature_preview_video_${index}`}
|
||||
id={index !== undefined ? `premium_feature_preview_video_${index}` : undefined}
|
||||
>
|
||||
<img src={DeviceFrame} alt="" className={styles.frame} draggable={false} />
|
||||
<canvas ref={thumbnailRef} className={styles.video} />
|
||||
<OptimizedVideo
|
||||
canPlay={isActive}
|
||||
className={buildClassName(styles.video, transitionClassNames)}
|
||||
src={mediaData}
|
||||
disablePictureInPicture
|
||||
playsInline
|
||||
muted
|
||||
loop
|
||||
/>
|
||||
{!videoId && <div className={styles.placeholder} />}
|
||||
{videoThumbnail && <canvas ref={thumbnailRef} className={styles.video} />}
|
||||
{videoId && (
|
||||
<OptimizedVideo
|
||||
canPlay={Boolean(isActive)}
|
||||
className={buildClassName(styles.video, transitionClassNames)}
|
||||
src={mediaData}
|
||||
disablePictureInPicture
|
||||
playsInline
|
||||
muted
|
||||
loop
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -19,6 +19,7 @@ import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
|
||||
import DeleteAccountModal from './deleteAccount/DeleteAccountModal.async';
|
||||
import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async';
|
||||
import FrozenAccountModal from './frozenAccount/FrozenAccountModal.async';
|
||||
import AboutStarGiftModal from './gift/AboutStarGiftModal.async';
|
||||
import GiftAuctionAcquiredModal from './gift/auction/GiftAuctionAcquiredModal.async';
|
||||
import GiftAuctionBidModal from './gift/auction/GiftAuctionBidModal.async';
|
||||
import GiftAuctionChangeRecipientModal from './gift/auction/GiftAuctionChangeRecipientModal.async';
|
||||
@ -105,6 +106,7 @@ type ModalKey = keyof Pick<TabState,
|
||||
'giftAuctionChangeRecipientModal' |
|
||||
'giftAuctionAcquiredModal' |
|
||||
'starGiftPriceDecreaseInfoModal' |
|
||||
'aboutStarGiftModal' |
|
||||
'monetizationVerificationModal' |
|
||||
'giftWithdrawModal' |
|
||||
'preparedMessageModal' |
|
||||
@ -177,6 +179,7 @@ const MODALS: ModalRegistry = {
|
||||
giftAuctionChangeRecipientModal: GiftAuctionChangeRecipientModal,
|
||||
giftAuctionAcquiredModal: GiftAuctionAcquiredModal,
|
||||
starGiftPriceDecreaseInfoModal: StarGiftPriceDecreaseInfoModal,
|
||||
aboutStarGiftModal: AboutStarGiftModal,
|
||||
monetizationVerificationModal: VerificationMonetizationModal,
|
||||
giftWithdrawModal: GiftWithdrawModal,
|
||||
giftStatusInfoModal: GiftStatusInfoModal,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { memo, type TeactNode } from '../../../lib/teact/teact';
|
||||
|
||||
import type { IconName } from '../../../types/icons';
|
||||
import type { OwnProps as ButtonProps } from '../../ui/Button';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
@ -25,6 +26,7 @@ type OwnProps = {
|
||||
footer?: TeactNode;
|
||||
buttonText?: TeactNode;
|
||||
hasBackdrop?: boolean;
|
||||
absoluteCloseButtonColor?: ButtonProps['color'];
|
||||
withSeparator?: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
onButtonClick?: NoneToVoidFunction;
|
||||
@ -40,6 +42,7 @@ const TableAboutModal = ({
|
||||
footer,
|
||||
buttonText,
|
||||
hasBackdrop,
|
||||
absoluteCloseButtonColor,
|
||||
withSeparator,
|
||||
contentClassName,
|
||||
onClose,
|
||||
@ -51,7 +54,7 @@ const TableAboutModal = ({
|
||||
className={buildClassName(styles.root, className)}
|
||||
contentClassName={buildClassName(styles.content, contentClassName)}
|
||||
hasAbsoluteCloseButton
|
||||
absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
|
||||
absoluteCloseButtonColor={absoluteCloseButtonColor || (hasBackdrop ? 'translucent-white' : undefined)}
|
||||
onClose={onClose}
|
||||
>
|
||||
{headerIconName && (
|
||||
|
||||
@ -30,6 +30,8 @@ type OwnProps = {
|
||||
contentClassName?: string;
|
||||
tableClassName?: string;
|
||||
hasBackdrop?: boolean;
|
||||
closeButtonColor?: 'translucent' | 'translucent-white';
|
||||
moreMenuItems?: TeactNode;
|
||||
onClose: NoneToVoidFunction;
|
||||
onButtonClick?: NoneToVoidFunction;
|
||||
withBalanceBar?: boolean;
|
||||
@ -50,6 +52,8 @@ const TableInfoModal = ({
|
||||
contentClassName,
|
||||
tableClassName,
|
||||
hasBackdrop,
|
||||
closeButtonColor,
|
||||
moreMenuItems,
|
||||
onClose,
|
||||
onButtonClick,
|
||||
withBalanceBar,
|
||||
@ -68,12 +72,13 @@ const TableInfoModal = ({
|
||||
isOpen={isOpen}
|
||||
hasCloseButton={Boolean(title)}
|
||||
hasAbsoluteCloseButton={!title}
|
||||
absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
|
||||
absoluteCloseButtonColor={closeButtonColor || (hasBackdrop ? 'translucent-white' : undefined)}
|
||||
isSlim
|
||||
header={modalHeader}
|
||||
title={title}
|
||||
className={className}
|
||||
contentClassName={buildClassName(styles.content, contentClassName)}
|
||||
moreMenuItems={moreMenuItems}
|
||||
onClose={onClose}
|
||||
withBalanceBar={withBalanceBar}
|
||||
currencyInBalanceBar={currencyInBalanceBar}
|
||||
|
||||
14
src/components/modals/gift/AboutStarGiftModal.async.tsx
Normal file
14
src/components/modals/gift/AboutStarGiftModal.async.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import type { OwnProps } from './AboutStarGiftModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const AboutStarGiftModalAsync = (props: OwnProps) => {
|
||||
const { modal } = props;
|
||||
const AboutStarGiftModal = useModuleLoader(Bundles.Stars, 'AboutStarGiftModal', !modal);
|
||||
|
||||
return AboutStarGiftModal ? <AboutStarGiftModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default AboutStarGiftModalAsync;
|
||||
53
src/components/modals/gift/AboutStarGiftModal.module.scss
Normal file
53
src/components/modals/gift/AboutStarGiftModal.module.scss
Normal file
@ -0,0 +1,53 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
width: calc(100% + 2rem);
|
||||
margin: -1rem -1rem 0.25rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.videoPreviewWrapper {
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
aspect-ratio: 10 / 8;
|
||||
width: 100%;
|
||||
|
||||
background: var(--premium-gradient);
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-top: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
padding-top: 0.25rem;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.understoodIcon {
|
||||
font-size: 1.1875rem;
|
||||
}
|
||||
93
src/components/modals/gift/AboutStarGiftModal.tsx
Normal file
93
src/components/modals/gift/AboutStarGiftModal.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Sparkles from '../../common/Sparkles';
|
||||
import PremiumFeaturePreviewVideo from '../../main/premium/previews/PremiumFeaturePreviewVideo';
|
||||
import Button from '../../ui/Button';
|
||||
import TableAboutModal, { type TableAboutData } from '../common/TableAboutModal';
|
||||
|
||||
import styles from './AboutStarGiftModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['aboutStarGiftModal'];
|
||||
};
|
||||
|
||||
const AboutStarGiftModal = ({
|
||||
modal,
|
||||
}: OwnProps) => {
|
||||
const { closeAboutStarGiftModal } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(modal?.isOpen);
|
||||
const renderingModal = useCurrentOrPrev(modal);
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeAboutStarGiftModal();
|
||||
});
|
||||
|
||||
const header = useMemo(() => {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.videoPreviewWrapper}>
|
||||
<Sparkles preset="progress" className={styles.sparkles} />
|
||||
<PremiumFeaturePreviewVideo
|
||||
videoId={renderingModal?.videoId}
|
||||
videoThumbnail={renderingModal?.videoThumbnail}
|
||||
isActive={isOpen}
|
||||
isDown
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{lang('StarGiftInfoTitle')}
|
||||
</div>
|
||||
<div className={styles.subtitle}>
|
||||
{lang('StarGiftInfoSubtitle')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [lang, renderingModal, isOpen]);
|
||||
|
||||
const footer = useMemo(() => {
|
||||
if (!isOpen) return undefined;
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
iconName="understood"
|
||||
iconClassName={styles.understoodIcon}
|
||||
onClick={handleClose}
|
||||
>
|
||||
{lang('ButtonUnderstood')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, [lang, isOpen, handleClose]);
|
||||
|
||||
const listItemData = useMemo(() => {
|
||||
return [
|
||||
['diamond', lang('StarGiftInfoUniqueTitle'), lang('StarGiftInfoUniqueSubtitle')],
|
||||
['auction', lang('StarGiftInfoTradableTitle'), lang('StarGiftInfoTradableSubtitle')],
|
||||
['crown-wear-outline', lang('StarGiftInfoWearableTitle'), lang('StarGiftInfoWearableSubtitle')],
|
||||
] satisfies TableAboutData;
|
||||
}, [lang]);
|
||||
|
||||
return (
|
||||
<TableAboutModal
|
||||
isOpen={isOpen}
|
||||
contentClassName={styles.content}
|
||||
header={header}
|
||||
listItemData={listItemData}
|
||||
footer={footer}
|
||||
hasBackdrop={Boolean(renderingModal?.videoId)}
|
||||
absoluteCloseButtonColor="translucent-white"
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AboutStarGiftModal);
|
||||
@ -25,6 +25,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.withBadge {
|
||||
--_height: 17rem;
|
||||
}
|
||||
|
||||
:global {
|
||||
canvas {
|
||||
transform-origin: center 45%;
|
||||
@ -33,17 +37,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.subtitleBadge, .badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
line-height: 0.875rem;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(50px);
|
||||
}
|
||||
|
||||
.subtitleBadge {
|
||||
margin-top: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
padding-block: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
line-height: 1rem;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(50px);
|
||||
|
||||
transition: color 150ms ease-in, background-color 0.15s !important;
|
||||
|
||||
&:hover {
|
||||
@ -97,6 +107,10 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge + .title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
line-height: 1.375rem;
|
||||
@ -106,7 +120,16 @@
|
||||
transition: color 150ms ease-in;
|
||||
}
|
||||
|
||||
.title, .subtitle {
|
||||
.title, .subtitle, .badge {
|
||||
z-index: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-top: auto;
|
||||
padding: 0.25rem 0.75rem;
|
||||
|
||||
font-size: 0.8125rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ type OwnProps = {
|
||||
backdropAttribute: ApiStarGiftAttributeBackdrop;
|
||||
patternAttribute: ApiStarGiftAttributePattern;
|
||||
title?: string;
|
||||
badge?: TeactNode;
|
||||
subtitle?: TeactNode;
|
||||
subtitlePeer?: ApiPeer;
|
||||
className?: string;
|
||||
@ -50,6 +51,7 @@ const UniqueGiftHeader = ({
|
||||
backdropAttribute,
|
||||
patternAttribute,
|
||||
title,
|
||||
badge,
|
||||
subtitle,
|
||||
subtitlePeer,
|
||||
className,
|
||||
@ -89,6 +91,7 @@ const UniqueGiftHeader = ({
|
||||
<div className={buildClassName(styles.root,
|
||||
'interactive-gift',
|
||||
showManageButtons && styles.withManageButtons,
|
||||
badge && styles.withBadge,
|
||||
className)}
|
||||
>
|
||||
<Transition
|
||||
@ -108,6 +111,9 @@ const UniqueGiftHeader = ({
|
||||
onMouseLeave={!IS_TOUCH_ENV ? unmarkGiftHover : undefined}
|
||||
/>
|
||||
</Transition>
|
||||
{Boolean(badge) && (
|
||||
<div className={styles.badge}>{badge}</div>
|
||||
)}
|
||||
{title && <h1 className={styles.title}>{title}</h1>}
|
||||
{Boolean(subtitle) && (
|
||||
<div
|
||||
|
||||
@ -80,3 +80,9 @@
|
||||
.starIcon {
|
||||
line-height: 1rem !important;
|
||||
}
|
||||
|
||||
.learnMoreLink {
|
||||
font-size: 0.875rem;
|
||||
color: white !important;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import {
|
||||
memo, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiStarGiftAuctionState } from '../../../../api/types';
|
||||
import type { TabState } from '../../../../global/types';
|
||||
|
||||
import { TME_LINK_PREFIX } from '../../../../config';
|
||||
import { selectTabState } from '../../../../global/selectors';
|
||||
import { copyTextToClipboard } from '../../../../util/clipboard';
|
||||
import { formatCountdown, formatDateTimeToString } from '../../../../util/dates/dateFormat';
|
||||
import { HOUR } from '../../../../util/dates/units';
|
||||
import { formatStarsAsIcon } from '../../../../util/localization/format';
|
||||
import { getServerTime } from '../../../../util/serverTime';
|
||||
import { getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import {
|
||||
getRandomGiftPreviewAttributes, getStickerFromGift, type GiftPreviewAttributes,
|
||||
preloadGiftAttributeStickers } from '../../../common/helpers/gifts';
|
||||
|
||||
import useInterval from '../../../../hooks/schedulers/useInterval';
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
@ -18,13 +25,16 @@ import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
|
||||
import Button from '../../../ui/Button';
|
||||
import Link from '../../../ui/Link';
|
||||
import MenuItem from '../../../ui/MenuItem';
|
||||
import TextTimer from '../../../ui/TextTimer';
|
||||
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
|
||||
import GiftItemStar from '../GiftItemStar';
|
||||
import UniqueGiftHeader from '../UniqueGiftHeader';
|
||||
|
||||
import styles from './GiftAuctionModal.module.scss';
|
||||
|
||||
const TEXT_TIMER_THRESHOLD = 48 * HOUR;
|
||||
const PREVIEW_UPDATE_INTERVAL = 3000;
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['giftAuctionModal'];
|
||||
@ -40,9 +50,15 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
setGiftModalSelectedGift,
|
||||
openGiftAuctionInfoModal,
|
||||
openGiftAuctionAcquiredModal,
|
||||
openAboutStarGiftModal,
|
||||
showNotification,
|
||||
openChatWithDraft,
|
||||
openUrl,
|
||||
openGiftInMarket,
|
||||
} = getActions();
|
||||
|
||||
const isOpen = Boolean(modal?.isOpen);
|
||||
const renderingModal = useCurrentOrPrev(modal);
|
||||
const renderingAuctionState = useCurrentOrPrev(auctionState);
|
||||
|
||||
const gift = renderingAuctionState?.gift;
|
||||
@ -50,14 +66,43 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
const userState = renderingAuctionState?.userState;
|
||||
const isFinished = state?.type === 'finished';
|
||||
|
||||
const [previewAttributes, setPreviewAttributes] = useState<GiftPreviewAttributes | undefined>();
|
||||
const shouldUseUniqueHeader = Boolean(gift && state && previewAttributes);
|
||||
|
||||
const uniqueHeaderRef = useRef<HTMLDivElement>();
|
||||
const lang = useLang();
|
||||
|
||||
const updatePreviewAttributes = useLastCallback(() => {
|
||||
if (!renderingModal?.sampleAttributes) return;
|
||||
setPreviewAttributes(getRandomGiftPreviewAttributes(renderingModal.sampleAttributes, previewAttributes));
|
||||
});
|
||||
|
||||
useInterval(updatePreviewAttributes, isOpen ? PREVIEW_UPDATE_INTERVAL : undefined, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && renderingModal?.sampleAttributes) {
|
||||
updatePreviewAttributes();
|
||||
} else {
|
||||
setPreviewAttributes(undefined);
|
||||
}
|
||||
}, [isOpen, renderingModal?.sampleAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
const attributes = renderingModal?.sampleAttributes;
|
||||
if (!attributes) return;
|
||||
preloadGiftAttributeStickers(attributes);
|
||||
}, [renderingModal?.sampleAttributes]);
|
||||
|
||||
const handleClose = useLastCallback(() => closeGiftAuctionModal());
|
||||
|
||||
const handleLearnMoreClick = useLastCallback(() => {
|
||||
openGiftAuctionInfoModal({});
|
||||
});
|
||||
|
||||
const handleLearnMoreAboutGiftsClick = useLastCallback(() => {
|
||||
openAboutStarGiftModal({});
|
||||
});
|
||||
|
||||
const handleItemsBoughtClick = useLastCallback(() => {
|
||||
if (!gift) return;
|
||||
const giftSticker = getStickerFromGift(gift);
|
||||
@ -70,8 +115,65 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
setGiftModalSelectedGift({ gift });
|
||||
});
|
||||
|
||||
const header = useMemo(() => {
|
||||
if (!gift || !state) {
|
||||
const auctionLink = useMemo(() => {
|
||||
if (!gift?.auctionSlug) return undefined;
|
||||
return `${TME_LINK_PREFIX}auction/${gift.auctionSlug}`;
|
||||
}, [gift]);
|
||||
|
||||
const handleCopyLink = useLastCallback(() => {
|
||||
if (!auctionLink) return;
|
||||
copyTextToClipboard(auctionLink);
|
||||
showNotification({
|
||||
message: lang('LinkCopied'),
|
||||
});
|
||||
});
|
||||
|
||||
const handleShareLink = useLastCallback(() => {
|
||||
if (!auctionLink) return;
|
||||
openChatWithDraft({ text: { text: auctionLink } });
|
||||
});
|
||||
|
||||
const handleOpenFragment = useLastCallback(() => {
|
||||
if (state?.type === 'finished' && state.fragmentListedUrl) {
|
||||
openUrl({ url: state.fragmentListedUrl });
|
||||
}
|
||||
});
|
||||
|
||||
const handleOpenTelegramMarket = useLastCallback(() => {
|
||||
if (!gift) return;
|
||||
handleClose();
|
||||
openGiftInMarket({ gift });
|
||||
});
|
||||
|
||||
const uniqueHeader = useMemo(() => {
|
||||
if (!shouldUseUniqueHeader) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const giftTitle = gift!.title || lang('StarGift');
|
||||
const badge = isFinished ? lang('GiftAuctionEnded') : lang('GiftAuctionInfoTitle');
|
||||
const subtitle = (
|
||||
<Link className={styles.learnMoreLink} isPrimary onClick={handleLearnMoreAboutGiftsClick}>
|
||||
{lang('GiftAuctionLearnMoreAboutGifts')}
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={uniqueHeaderRef}>
|
||||
<UniqueGiftHeader
|
||||
modelAttribute={previewAttributes!.model}
|
||||
backdropAttribute={previewAttributes!.backdrop}
|
||||
patternAttribute={previewAttributes!.pattern}
|
||||
title={giftTitle}
|
||||
badge={badge}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}, [shouldUseUniqueHeader, gift, isFinished, lang, previewAttributes, handleLearnMoreAboutGiftsClick]);
|
||||
|
||||
const regularHeader = useMemo(() => {
|
||||
if (!gift || !state || shouldUseUniqueHeader) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -97,7 +199,9 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [gift, state, isFinished, lang, handleLearnMoreClick]);
|
||||
}, [gift, state, shouldUseUniqueHeader, isFinished, lang, handleLearnMoreClick]);
|
||||
|
||||
const header = uniqueHeader || regularHeader;
|
||||
|
||||
const modalData = useMemo(() => {
|
||||
if (!gift || !state || !userState) {
|
||||
@ -148,6 +252,10 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
const auctionTimeLeft = state.endDate - getServerTime();
|
||||
const shouldUseTextTimer = auctionTimeLeft > 0 && auctionTimeLeft < TEXT_TIMER_THRESHOLD;
|
||||
|
||||
const canBuyOnFragment = state.type === 'finished'
|
||||
&& Boolean(state.fragmentListedUrl && state.fragmentListedCount);
|
||||
const canBuyOnTelegram = state.type === 'finished' && Boolean(state.listedCount);
|
||||
|
||||
const footer = (
|
||||
<div className={styles.footer}>
|
||||
{acquiredCount > 0 && (
|
||||
@ -165,6 +273,40 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
}, { pluralValue: acquiredCount, withNodes: true })}
|
||||
</Link>
|
||||
)}
|
||||
{canBuyOnFragment && (
|
||||
<Link className={styles.itemsBoughtLink} isPrimary onClick={handleOpenFragment}>
|
||||
{lang('GiftAuctionForSaleOnFragment', {
|
||||
count: giftSticker ? (
|
||||
<>
|
||||
{lang.number(state.fragmentListedCount!)}
|
||||
<AnimatedIconFromSticker
|
||||
className={styles.itemsBoughtSticker}
|
||||
sticker={giftSticker}
|
||||
size={20}
|
||||
play={false}
|
||||
/>
|
||||
</>
|
||||
) : lang.number(state.fragmentListedCount!),
|
||||
}, { withNodes: true })}
|
||||
</Link>
|
||||
)}
|
||||
{canBuyOnTelegram && (
|
||||
<Link className={styles.itemsBoughtLink} isPrimary onClick={handleOpenTelegramMarket}>
|
||||
{lang('GiftAuctionForSaleOnTelegram', {
|
||||
count: giftSticker ? (
|
||||
<>
|
||||
{lang.number(state.listedCount!)}
|
||||
<AnimatedIconFromSticker
|
||||
className={styles.itemsBoughtSticker}
|
||||
sticker={giftSticker}
|
||||
size={20}
|
||||
play={false}
|
||||
/>
|
||||
</>
|
||||
) : lang.number(state.listedCount!),
|
||||
}, { withNodes: true })}
|
||||
</Link>
|
||||
)}
|
||||
<Button
|
||||
noForcedUpperCase
|
||||
className={styles.footerButton}
|
||||
@ -194,7 +336,26 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
tableData,
|
||||
footer,
|
||||
};
|
||||
}, [gift, state, userState, isFinished, lang, handleJoinClick, handleItemsBoughtClick, handleClose]);
|
||||
}, [gift, state, userState, isFinished, lang, handleJoinClick, handleItemsBoughtClick, handleClose,
|
||||
handleOpenFragment, handleOpenTelegramMarket]);
|
||||
|
||||
const moreMenuItems = useMemo(() => {
|
||||
if (!shouldUseUniqueHeader) return undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem icon="info" onClick={handleLearnMoreClick}>
|
||||
{lang('GiftAuctionLearnMoreMenuItem')}
|
||||
</MenuItem>
|
||||
<MenuItem icon="link-badge" onClick={handleCopyLink}>
|
||||
{lang('CopyLink')}
|
||||
</MenuItem>
|
||||
<MenuItem icon="forward" onClick={handleShareLink}>
|
||||
{lang('Share')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
}, [shouldUseUniqueHeader, lang, handleLearnMoreClick, handleCopyLink, handleShareLink]);
|
||||
|
||||
return (
|
||||
<TableInfoModal
|
||||
@ -204,6 +365,8 @@ const GiftAuctionModal = ({ modal, auctionState }: OwnProps & StateProps) => {
|
||||
tableData={modalData?.tableData}
|
||||
className={styles.modal}
|
||||
contentClassName={styles.modalContent}
|
||||
closeButtonColor={shouldUseUniqueHeader ? 'translucent-white' : undefined}
|
||||
moreMenuItems={moreMenuItems}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -27,7 +27,6 @@ import { renderGiftOriginalInfo } from '../../../common/helpers/giftOriginalInfo
|
||||
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
@ -45,7 +44,6 @@ import Button from '../../../ui/Button';
|
||||
import Checkbox from '../../../ui/Checkbox';
|
||||
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
||||
import Link from '../../../ui/Link';
|
||||
import Menu from '../../../ui/Menu';
|
||||
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
|
||||
import UniqueGiftHeader from '../UniqueGiftHeader';
|
||||
|
||||
@ -108,16 +106,7 @@ const GiftInfoModal = ({
|
||||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState<boolean>(false);
|
||||
const [shouldPayInTon, setShouldPayInTon] = useState<boolean>(false);
|
||||
|
||||
const moreButtonRef = useRef<HTMLButtonElement>();
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
const uniqueGiftHeaderRef = useRef<HTMLDivElement>();
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuAnchor,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(moreButtonRef);
|
||||
|
||||
const handleSymbolClick = useLastCallback(() => {
|
||||
if (!gift || !giftAttributes?.pattern) return;
|
||||
@ -540,19 +529,6 @@ const GiftInfoModal = ({
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
<Button
|
||||
ref={moreButtonRef}
|
||||
className={styles.moreMenuButton}
|
||||
round
|
||||
color="translucent-white"
|
||||
size="tiny"
|
||||
iconName="more"
|
||||
aria-haspopup="menu"
|
||||
aria-label={lang('AriaMoreButton')}
|
||||
onContextMenu={handleContextMenu}
|
||||
onClick={handleContextMenu}
|
||||
/>
|
||||
|
||||
{Boolean(resellPrice?.amount) && (
|
||||
<div className={styles.giftResalePriceContainer}>
|
||||
{resellPrice.currency === TON_CURRENCY_CODE
|
||||
@ -881,38 +857,16 @@ const GiftInfoModal = ({
|
||||
isGiftUnique, saleDateInfo,
|
||||
canBuyGift, giftOwnerTitle, resellPrice, giftSubtitle,
|
||||
releasedByPeer, handleSymbolClick, handleBackdropClick, handleModelClick,
|
||||
handleContextMenu,
|
||||
]);
|
||||
|
||||
const getRootElement = useLastCallback(() => uniqueGiftHeaderRef.current);
|
||||
const getTriggerElement = useLastCallback(() => moreButtonRef.current);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const uniqueGiftContextMenu = contextMenuAnchor && typeGift && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
className="gift-context-menu with-menu-transitions"
|
||||
autoClose
|
||||
withPortal
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
positionX="right"
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
>
|
||||
<GiftMenuItems
|
||||
peerId={renderingModal!.peerId!}
|
||||
gift={typeGift}
|
||||
canManage={canManage}
|
||||
collectibleEmojiStatuses={collectibleEmojiStatuses}
|
||||
currentUserEmojiStatus={currentUserEmojiStatus}
|
||||
/>
|
||||
</Menu>
|
||||
const moreMenuItems = typeGift && (
|
||||
<GiftMenuItems
|
||||
peerId={renderingModal!.peerId!}
|
||||
gift={typeGift}
|
||||
canManage={canManage}
|
||||
collectibleEmojiStatuses={collectibleEmojiStatuses}
|
||||
currentUserEmojiStatus={currentUserEmojiStatus}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
@ -926,12 +880,13 @@ const GiftInfoModal = ({
|
||||
footer={modalData?.footer}
|
||||
className={styles.modal}
|
||||
contentClassName={styles.modalContent}
|
||||
closeButtonColor={isGiftUnique ? 'translucent-white' : undefined}
|
||||
moreMenuItems={moreMenuItems}
|
||||
onClose={handleClose}
|
||||
withBalanceBar={Boolean(canBuyGift)}
|
||||
currencyInBalanceBar={confirmPrice?.currency}
|
||||
isLowStackPriority
|
||||
/>
|
||||
{uniqueGiftContextMenu}
|
||||
{uniqueGift && currentUser && Boolean(confirmPrice) && (
|
||||
<ConfirmDialog
|
||||
isOpen={isConfirmModalOpen}
|
||||
|
||||
@ -3,18 +3,14 @@ import {
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiPeer,
|
||||
ApiStarGiftAttributeModel,
|
||||
} from '../../../../api/types';
|
||||
import type { ApiPeer } from '../../../../api/types';
|
||||
import type { TabState } from '../../../../global/types';
|
||||
import { ApiMediaFormat } from '../../../../api/types';
|
||||
|
||||
import { getStickerMediaHash } from '../../../../global/helpers';
|
||||
import { getPeerTitle } from '../../../../global/helpers/peers';
|
||||
import { selectPeer } from '../../../../global/selectors';
|
||||
import { fetch } from '../../../../util/mediaLoader';
|
||||
import { getRandomGiftPreviewAttributes, type GiftPreviewAttributes } from '../../../common/helpers/gifts';
|
||||
import {
|
||||
getRandomGiftPreviewAttributes, type GiftPreviewAttributes,
|
||||
preloadGiftAttributeStickers } from '../../../common/helpers/gifts';
|
||||
|
||||
import useInterval from '../../../../hooks/schedulers/useInterval';
|
||||
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
|
||||
@ -125,19 +121,10 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
||||
}
|
||||
}, [isOpen, renderingModal?.sampleAttributes]);
|
||||
|
||||
// Preload stickers and patterns
|
||||
useEffect(() => {
|
||||
const attributes = renderingModal?.sampleAttributes;
|
||||
if (!attributes) return;
|
||||
const patternStickers = attributes.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'pattern')
|
||||
.map((attr) => attr.sticker);
|
||||
const modelStickers = attributes.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model')
|
||||
.map((attr) => attr.sticker);
|
||||
|
||||
const mediaHashes = [...patternStickers, ...modelStickers].map((sticker) => getStickerMediaHash(sticker, 'full'));
|
||||
mediaHashes.forEach((hash) => {
|
||||
fetch(hash, ApiMediaFormat.BlobUrl);
|
||||
});
|
||||
preloadGiftAttributeStickers(attributes);
|
||||
}, [renderingModal?.sampleAttributes]);
|
||||
|
||||
const formattedPriceElement = useMemo(() => (upgradeStars ? (
|
||||
|
||||
@ -248,4 +248,11 @@
|
||||
top: 0.875rem;
|
||||
left: 0.875rem;
|
||||
}
|
||||
|
||||
.modal-more-button {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0.875rem;
|
||||
right: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ElementRef, FC, TeactNode } from '../../lib/teact/teact';
|
||||
import type React from '../../lib/teact/teact';
|
||||
import { beginHeavyAnimation, useEffect } from '../../lib/teact/teact';
|
||||
import { beginHeavyAnimation, useEffect, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type { TextPart } from '../../types';
|
||||
|
||||
@ -9,6 +9,7 @@ import captureKeyboardListeners from '../../util/captureKeyboardListeners';
|
||||
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
|
||||
import trapFocus from '../../util/trapFocus';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
|
||||
@ -16,6 +17,7 @@ import useOldLang from '../../hooks/useOldLang';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
|
||||
import Button, { type OwnProps as ButtonProps } from './Button';
|
||||
import Menu from './Menu';
|
||||
import ModalStarBalanceBar from './ModalStarBalanceBar';
|
||||
import Portal from './Portal';
|
||||
|
||||
@ -44,6 +46,7 @@ export type OwnProps = {
|
||||
isLowStackPriority?: boolean;
|
||||
dialogContent?: React.ReactNode;
|
||||
ignoreFreeze?: boolean;
|
||||
moreMenuItems?: TeactNode;
|
||||
onClose: () => void;
|
||||
onCloseAnimationEnd?: () => void;
|
||||
onEnter?: () => void;
|
||||
@ -72,6 +75,7 @@ const Modal: FC<OwnProps> = ({
|
||||
isLowStackPriority,
|
||||
dialogContent,
|
||||
dialogClassName,
|
||||
moreMenuItems,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
onEnter,
|
||||
@ -88,6 +92,25 @@ const Modal: FC<OwnProps> = ({
|
||||
withShouldRender: true,
|
||||
});
|
||||
|
||||
const localDialogRef = useRef<HTMLDivElement>();
|
||||
const moreButtonRef = useRef<HTMLButtonElement>();
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuAnchor,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(moreButtonRef);
|
||||
|
||||
const actualDialogRef = dialogRef || localDialogRef;
|
||||
|
||||
const getRootElement = useLastCallback(() => actualDialogRef.current);
|
||||
const getTriggerElement = useLastCallback(() => moreButtonRef.current);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const withCloseButton = hasCloseButton || hasAbsoluteCloseButton;
|
||||
|
||||
useEffect(() => {
|
||||
@ -193,8 +216,39 @@ const Modal: FC<OwnProps> = ({
|
||||
)}
|
||||
<div className="modal-container">
|
||||
<div className="modal-backdrop" onClick={!noBackdropClose ? onClose : undefined} />
|
||||
<div className={modalDialogClassName} ref={dialogRef} style={dialogStyle}>
|
||||
<div className={modalDialogClassName} ref={actualDialogRef} style={dialogStyle}>
|
||||
{renderHeader()}
|
||||
{Boolean(moreMenuItems) && (
|
||||
<>
|
||||
<Button
|
||||
ref={moreButtonRef}
|
||||
className="modal-more-button"
|
||||
round
|
||||
color={absoluteCloseButtonColor}
|
||||
size="tiny"
|
||||
iconName="more"
|
||||
ariaLabel={lang('AriaMoreButton')}
|
||||
onClick={handleContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
/>
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
autoClose
|
||||
withPortal
|
||||
positionX="right"
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
getRootElement={getRootElement}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
>
|
||||
{moreMenuItems}
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
{dialogContent}
|
||||
<div className={buildClassName('modal-content custom-scroll', contentClassName)} style={style}>
|
||||
{children}
|
||||
|
||||
@ -583,11 +583,17 @@ addActionHandler('shiftGiftUpgradeNextPrice', async (global, _actions, payload):
|
||||
addActionHandler('openGiftAuctionModal', async (global, _actions, payload): Promise<void> => {
|
||||
const { gift, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
await getPromiseActions().loadActiveGiftAuction({ giftId: gift.id, tabId });
|
||||
const [, preview] = await Promise.all([
|
||||
getPromiseActions().loadActiveGiftAuction({ giftId: gift.id, tabId }),
|
||||
callApi('fetchStarGiftUpgradePreview', { giftId: gift.id }),
|
||||
]);
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
giftAuctionModal: { isOpen: true },
|
||||
giftAuctionModal: {
|
||||
isOpen: true,
|
||||
sampleAttributes: preview?.sampleAttributes,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -42,6 +42,7 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe
|
||||
actions.closeStarsBalanceModal({ tabId });
|
||||
actions.closeStarsTransactionModal({ tabId });
|
||||
actions.closeGiftInfoModal({ tabId });
|
||||
actions.closeGiftAuctionModal({ tabId });
|
||||
|
||||
if (!currentMessageList || (
|
||||
currentMessageList.chatId !== chatId
|
||||
|
||||
@ -441,6 +441,32 @@ addActionHandler('openGiftAuctionInfoModal', (global, _actions, payload): Action
|
||||
|
||||
addTabStateResetterAction('closeGiftAuctionInfoModal', 'giftAuctionInfoModal');
|
||||
|
||||
addActionHandler('openAboutStarGiftModal', async (global, _actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const result = await callApi('fetchPremiumPromo');
|
||||
|
||||
let videoId: string | undefined;
|
||||
let videoThumbnail;
|
||||
|
||||
if (result?.promo) {
|
||||
const giftsIndex = result.promo.videoSections.indexOf('gifts');
|
||||
if (giftsIndex !== -1 && giftsIndex < result.promo.videos.length) {
|
||||
const video = result.promo.videos[giftsIndex];
|
||||
videoId = video.id;
|
||||
videoThumbnail = video.thumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
aboutStarGiftModal: { isOpen: true, videoId, videoThumbnail },
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addTabStateResetterAction('closeAboutStarGiftModal', 'aboutStarGiftModal');
|
||||
|
||||
addActionHandler('openGiftAuctionChangeRecipientModal', (global, _actions, payload): ActionReturnType => {
|
||||
const {
|
||||
oldPeerId, newPeerId, message, shouldHideName, tabId = getCurrentTabId(),
|
||||
|
||||
@ -2709,6 +2709,8 @@ export interface ActionPayloads {
|
||||
closeGiftAuctionBidModal: WithTabId | undefined;
|
||||
openGiftAuctionInfoModal: WithTabId | undefined;
|
||||
closeGiftAuctionInfoModal: WithTabId | undefined;
|
||||
openAboutStarGiftModal: WithTabId | undefined;
|
||||
closeAboutStarGiftModal: WithTabId | undefined;
|
||||
openGiftAuctionChangeRecipientModal: {
|
||||
oldPeerId: string;
|
||||
newPeerId: string;
|
||||
|
||||
@ -53,6 +53,7 @@ import type {
|
||||
ApiStarsTransaction,
|
||||
ApiStarTopupOption,
|
||||
ApiSticker,
|
||||
ApiThumbnail,
|
||||
ApiTypeCurrencyAmount,
|
||||
ApiTypePrepaidGiveaway,
|
||||
ApiTypeStoryView,
|
||||
@ -895,6 +896,7 @@ export type TabState = {
|
||||
|
||||
giftAuctionModal?: {
|
||||
isOpen?: boolean;
|
||||
sampleAttributes?: ApiStarGiftAttribute[];
|
||||
};
|
||||
|
||||
giftAuctionBidModal?: {
|
||||
@ -908,6 +910,12 @@ export type TabState = {
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
aboutStarGiftModal?: {
|
||||
isOpen?: boolean;
|
||||
videoId?: string;
|
||||
videoThumbnail?: ApiThumbnail;
|
||||
};
|
||||
|
||||
giftAuctionChangeRecipientModal?: {
|
||||
isOpen?: boolean;
|
||||
oldPeerId?: string;
|
||||
|
||||
16
src/types/language.d.ts
vendored
16
src/types/language.d.ts
vendored
@ -1830,6 +1830,16 @@ export interface LangPair {
|
||||
'GiftAuctionChangeRecipientTitle': undefined;
|
||||
'GiftAuctionAveragePrice': undefined;
|
||||
'GiftAuctionTapToBidMore': undefined;
|
||||
'GiftAuctionLearnMoreAboutGifts': undefined;
|
||||
'GiftAuctionLearnMoreMenuItem': undefined;
|
||||
'StarGiftInfoTitle': undefined;
|
||||
'StarGiftInfoSubtitle': undefined;
|
||||
'StarGiftInfoUniqueTitle': undefined;
|
||||
'StarGiftInfoUniqueSubtitle': undefined;
|
||||
'StarGiftInfoTradableTitle': undefined;
|
||||
'StarGiftInfoTradableSubtitle': undefined;
|
||||
'StarGiftInfoWearableTitle': undefined;
|
||||
'StarGiftInfoWearableSubtitle': undefined;
|
||||
'StarGift': undefined;
|
||||
'SettingsItemPrivacyPasskeys': undefined;
|
||||
'SettingsItemPrivacyOn': undefined;
|
||||
@ -3145,6 +3155,12 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'RatingLevel': {
|
||||
'level': V;
|
||||
};
|
||||
'GiftAuctionForSaleOnFragment': {
|
||||
'count': V;
|
||||
};
|
||||
'GiftAuctionForSaleOnTelegram': {
|
||||
'count': V;
|
||||
};
|
||||
'GiftLockedMessage': {
|
||||
'relativeDate': V;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user