Gift Upgrade: Support gift upgrade price info modal (#6511)

This commit is contained in:
Alexander Zinchuk 2025-12-08 17:39:27 +01:00
parent 9e08371b9c
commit a0bf570dd6
20 changed files with 453 additions and 29 deletions

View File

@ -9,6 +9,8 @@ import type {
ApiStarGiftAttributeCounter,
ApiStarGiftAttributeId,
ApiStarGiftCollection,
ApiStarGiftUpgradePreview,
ApiStarGiftUpgradePrice,
ApiTypeResaleStarGifts,
} from '../../types';
@ -315,3 +317,20 @@ export function buildApiStarGiftCollection(collection: GramJs.StarGiftCollection
hash: hash.toString(),
};
}
export function buildApiStarGiftUpgradePrice(price: GramJs.StarGiftUpgradePrice): ApiStarGiftUpgradePrice {
return {
date: price.date,
upgradeStars: toJSNumber(price.upgradeStars),
};
}
export function buildApiStarGiftUpgradePreview(
result: GramJs.payments.StarGiftUpgradePreview,
): ApiStarGiftUpgradePreview {
return {
sampleAttributes: result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean),
prices: result.prices?.map(buildApiStarGiftUpgradePrice) || [],
nextPrices: result.nextPrices?.map(buildApiStarGiftUpgradePrice) || [],
};
}

View File

@ -14,8 +14,14 @@ import { buildApiChatFromPreview } from '../apiBuilders/chats';
import {
buildApiFormattedText,
} from '../apiBuilders/common';
import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift,
buildApiStarGiftAttribute, buildApiStarGiftCollection, buildInputResaleGiftsAttributes } from '../apiBuilders/gifts';
import {
buildApiResaleGifts,
buildApiSavedStarGift,
buildApiStarGift,
buildApiStarGiftCollection,
buildApiStarGiftUpgradePreview,
buildInputResaleGiftsAttributes,
} from '../apiBuilders/gifts';
import {
buildApiCurrencyAmount,
buildApiStarsGiftOptions,
@ -403,7 +409,7 @@ export async function fetchStarGiftUpgradePreview({
return undefined;
}
return result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean);
return buildApiStarGiftUpgradePreview(result);
}
export function upgradeStarGift({

View File

@ -89,6 +89,17 @@ export interface ApiStarGiftAttributeOriginalDetails {
export type ApiStarGiftAttribute = ApiStarGiftAttributeModel | ApiStarGiftAttributePattern
| ApiStarGiftAttributeBackdrop | ApiStarGiftAttributeOriginalDetails;
export interface ApiStarGiftUpgradePrice {
date: number;
upgradeStars: number;
}
export interface ApiStarGiftUpgradePreview {
sampleAttributes: ApiStarGiftAttribute[];
prices: ApiStarGiftUpgradePrice[];
nextPrices: ApiStarGiftUpgradePrice[];
}
export interface ApiSavedStarGift {
isNameHidden?: boolean;
isUnsaved?: boolean;

View File

@ -2398,3 +2398,9 @@
"StealthModeButtonToStory" = "Enable and Open the Story";
"StealthModeButtonRecharge" = "Available in {timer}";
"StealthModeComposerPlaceholder" = "Stealth Mode active {timer}";
"UsersWhoUpgradeFirst" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
"UpgradeCostDrops" = "Upgrade cost drops every minute.";
"StarGiftPriceDecreaseInfoLink" = "See how price will decrease >";
"StarGiftUpgradeCostModalTitle" = "Upgrade Cost";
"StarGiftUpgradeCostHint" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
"StarGiftPriceDecreaseTimer" = "Price decreases in {timer}";

View File

@ -12,6 +12,7 @@ export { default as GiftInfoValueModal } from '../components/modals/gift/value/G
export { default as GiftLockedModal } from '../components/modals/gift/locked/GiftLockedModal';
export { default as GiftResalePriceComposerModal } from '../components/modals/gift/resale/GiftResalePriceComposerModal';
export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal';
export { default as StarGiftPriceDecreaseInfoModal } from '../components/modals/gift/StarGiftPriceDecreaseInfoModal';
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';

View File

@ -1,5 +1,7 @@
/* stylelint-disable plugin/no-low-performance-animation-properties */
$progress-color: #7e85ff;
.root {
--percent: calc(var(--progress, 0.5) * 100%);
@ -81,7 +83,7 @@
color: #ffffff;
background-color: #7E85FF;
background-color: $progress-color;
transition: width 0.25s ease-in-out;
@ -110,7 +112,7 @@
display: inline-block;
color: #7E85FF;
color: $progress-color;
transition: left 0.3s ease;
}
@ -251,6 +253,34 @@
}
}
.inverted {
.positiveLayer,
.positiveProgress {
--multiplier: calc(1 / var(--positive-progress) - 1);
right: 0;
left: auto;
background-position: 100% 0;
background-size: calc(100% / var(--positive-progress)) 100%;
.left {
left: calc(-100% * var(--multiplier) + 0.75rem);
}
.right {
right: 0.75rem;
}
}
}
.noGradient {
.positiveLayer,
.positiveProgress {
background-color: $progress-color;
background-image: none;
}
}
.transitioning {
.left,
.right {

View File

@ -31,6 +31,8 @@ type OwnProps = {
progress?: number;
isPrimary?: boolean;
isNegative?: boolean;
isInverted?: boolean;
shouldSkipGradient?: boolean;
animationDirection?: AnimationDirection;
className?: string;
};
@ -43,6 +45,8 @@ const PremiumProgress: FC<OwnProps> = ({
progress = 0,
isPrimary,
isNegative,
isInverted,
shouldSkipGradient,
animationDirection = 'none',
className,
}) => {
@ -89,7 +93,8 @@ const PremiumProgress: FC<OwnProps> = ({
const minBadgeShift = halfBadgeWidth;
const maxBadgeShift = parentWidth - halfBadgeWidth;
const halfBeakWidth = BEAK_WIDTH_PX / 2;
const currentShift = isNegative ? (1 - badgeProgress) * parentWidth : badgeProgress * parentWidth;
const effectiveProgress = (isInverted || isNegative) ? (1 - badgeProgress) : badgeProgress;
const currentShift = effectiveProgress * parentWidth;
let safeShift = Math.max(minBadgeShift, Math.min(currentShift, maxBadgeShift));
if (currentShift < CORNER_BEAK_THRESHOLD) {
@ -107,7 +112,7 @@ const PremiumProgress: FC<OwnProps> = ({
}
};
useEffect(updateBadgePosition, [badgeProgress, badgeWidth, isNegative, CORNER_BEAK_THRESHOLD]);
useEffect(updateBadgePosition, [badgeProgress, badgeWidth, isNegative, isInverted, CORNER_BEAK_THRESHOLD]);
useResizeObserver(parentContainerRef, updateBadgePosition);
@ -260,6 +265,8 @@ const PremiumProgress: FC<OwnProps> = ({
hasFloatingBadge && styles.withBadge,
isPrimary && styles.primary,
isNegative && styles.negative,
isInverted && styles.inverted,
shouldSkipGradient && styles.noGradient,
shouldAnimateCaptions && styles.transitioning,
isCycling && styles.cycling,
className,

View File

@ -25,6 +25,7 @@ import GiftLockedModal from './gift/locked/GiftLockedModal.async';
import GiftDescriptionRemoveModal from './gift/message/GiftDescriptionRemoveModal.async';
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
import GiftResalePriceComposerModal from './gift/resale/GiftResalePriceComposerModal.async';
import StarGiftPriceDecreaseInfoModal from './gift/StarGiftPriceDecreaseInfoModal.async';
import GiftStatusInfoModal from './gift/status/GiftStatusInfoModal.async';
import GiftTransferConfirmModal from './gift/transfer/GiftTransferConfirmModal.async';
import GiftTransferModal from './gift/transfer/GiftTransferModal.async';
@ -92,6 +93,7 @@ type ModalKey = keyof Pick<TabState,
'locationAccessModal' |
'aboutAdsModal' |
'giftUpgradeModal' |
'starGiftPriceDecreaseInfoModal' |
'monetizationVerificationModal' |
'giftWithdrawModal' |
'preparedMessageModal' |
@ -156,6 +158,7 @@ const MODALS: ModalRegistry = {
locationAccessModal: LocationAccessModal,
aboutAdsModal: AboutAdsModal,
giftUpgradeModal: GiftUpgradeModal,
starGiftPriceDecreaseInfoModal: StarGiftPriceDecreaseInfoModal,
monetizationVerificationModal: VerificationMonetizationModal,
giftWithdrawModal: GiftWithdrawModal,
giftStatusInfoModal: GiftStatusInfoModal,

View File

@ -30,6 +30,7 @@ type OwnProps = {
buttonText?: string;
className?: string;
contentClassName?: string;
tableClassName?: string;
hasBackdrop?: boolean;
onClose: NoneToVoidFunction;
onButtonClick?: NoneToVoidFunction;
@ -49,6 +50,7 @@ const TableInfoModal = ({
buttonText,
className,
contentClassName,
tableClassName,
hasBackdrop,
onClose,
onButtonClick,
@ -82,7 +84,7 @@ const TableInfoModal = ({
<Avatar peer={headerAvatarPeer} size="jumbo" className={styles.avatar} />
)}
{header}
<div className={styles.table}>
<div className={buildClassName(styles.table, tableClassName)}>
{tableData?.map(([label, value]) => (
<>
{Boolean(label) && <div className={buildClassName(styles.cell, styles.title)}>{label}</div>}

View File

@ -0,0 +1,16 @@
import type { FC } from '../../../lib/teact/teact';
import type { OwnProps } from './StarGiftPriceDecreaseInfoModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const StarGiftPriceDecreaseInfoModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const StarGiftPriceDecreaseInfoModal = useModuleLoader(Bundles.Stars, 'StarGiftPriceDecreaseInfoModal', !modal);
return StarGiftPriceDecreaseInfoModal ? <StarGiftPriceDecreaseInfoModal {...props} /> : undefined;
};
export default StarGiftPriceDecreaseInfoModalAsync;

View File

@ -0,0 +1,49 @@
.header {
text-align: center;
}
.headerTitle {
margin-top: 1.5rem;
margin-bottom: 0;
font-size: 1.5rem;
font-weight: var(--font-weight-semibold);
}
.headerHint {
margin-top: 0.375rem;
margin-bottom: 0.5rem;
font-size: 1rem;
line-height: 1.375rem;
text-wrap: balance;
}
.starIconContainer {
display: inline-flex;
align-items: center;
line-height: 1rem;
}
.progress {
margin: 4.5rem auto 1.5rem;
}
.table {
overflow-y: auto;
max-height: 19.25rem;
}
.footerText {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-align: center;
}
.footer {
display: flex;
flex-direction: column;
}
.understoodIcon {
font-size: 1.25rem;
}

View File

@ -0,0 +1,95 @@
import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { TabState } from '../../../global/types';
import buildClassName from '../../../util/buildClassName';
import { formatDateTimeToString } from '../../../util/dates/dateFormat';
import { formatStarsAsIcon, formatStarsAsText } from '../../../util/localization/format';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import PremiumProgress from '../../common/PremiumProgress';
import Button from '../../ui/Button';
import TableInfoModal, { type TableData } from '../common/TableInfoModal';
import styles from './StarGiftPriceDecreaseInfoModal.module.scss';
export type OwnProps = {
modal: TabState['starGiftPriceDecreaseInfoModal'];
};
const StarGiftPriceDecreaseInfoModal = ({ modal }: OwnProps) => {
const { closeStarGiftPriceDecreaseInfoModal } = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const handleClose = useLastCallback(() => {
closeStarGiftPriceDecreaseInfoModal();
});
const tableData = useMemo(() => {
if (!renderingModal) return undefined;
const { prices } = renderingModal;
return prices.map((price): TableData[number] => [
formatDateTimeToString(price.date * 1000, lang.code, true, undefined, true),
formatStarsAsIcon(lang, price.upgradeStars, { containerClassName: styles.starIconContainer }),
]);
}, [lang, renderingModal]);
const footer = useMemo(() => {
if (!isOpen) return undefined;
return (
<div className={styles.footer}>
<p className={styles.footerText}>{lang('UpgradeCostDrops')}</p>
<Button
onClick={handleClose}
iconName="understood"
iconClassName={styles.understoodIcon}
>
{lang('ButtonUnderstood')}
</Button>
</div>
);
}, [lang, isOpen, handleClose]);
if (!tableData || !renderingModal) return undefined;
const { currentPrice, minPrice, maxPrice } = renderingModal;
const progress = maxPrice !== minPrice ? (currentPrice - minPrice) / (maxPrice - minPrice) : 0;
const header = (
<div className={styles.header}>
<PremiumProgress
leftText={formatStarsAsText(lang, maxPrice)}
rightText={formatStarsAsText(lang, minPrice)}
floatingBadgeText={formatStarsAsText(lang, currentPrice)}
floatingBadgeIcon="star"
progress={progress}
isInverted
shouldSkipGradient
className={styles.progress}
/>
<p className={styles.headerTitle}>{lang('StarGiftUpgradeCostModalTitle')}</p>
<p className={styles.headerHint}>{lang('StarGiftUpgradeCostHint')}</p>
</div>
);
return (
<TableInfoModal
isOpen={isOpen}
onClose={handleClose}
header={header}
tableData={tableData}
tableClassName={buildClassName(styles.table, 'custom-scroll')}
footer={footer}
/>
);
};
export default memo(StarGiftPriceDecreaseInfoModal);

View File

@ -8,3 +8,14 @@
.footerButton {
margin-top: 0.5rem;
}
.link {
display: flex;
justify-content: center;
}
.priceDecreaseTimer {
font-size: 0.875rem;
text-transform: none;
opacity: 0.6;
}

View File

@ -9,7 +9,6 @@ import type {
ApiStarGiftAttributeBackdrop,
ApiStarGiftAttributeModel,
ApiStarGiftAttributePattern,
ApiStarGiftRegular,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { ApiMediaFormat } from '../../../../api/types';
@ -17,7 +16,6 @@ import { ApiMediaFormat } from '../../../../api/types';
import { getStickerMediaHash } from '../../../../global/helpers';
import { getPeerTitle } from '../../../../global/helpers/peers';
import { selectPeer } from '../../../../global/selectors';
import { formatStarsAsIcon } from '../../../../util/localization/format';
import { fetch } from '../../../../util/mediaLoader';
import useInterval from '../../../../hooks/schedulers/useInterval';
@ -25,8 +23,12 @@ import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import AnimatedCounter from '../../../common/AnimatedCounter';
import Icon from '../../../common/icons/Icon';
import Button from '../../../ui/Button';
import Checkbox from '../../../ui/Checkbox';
import Link from '../../../ui/Link';
import TextTimer from '../../../ui/TextTimer';
import TableAboutModal, { type TableAboutData } from '../../common/TableAboutModal';
import UniqueGiftHeader from '../UniqueGiftHeader';
@ -49,7 +51,14 @@ type Attributes = {
const PREVIEW_UPDATE_INTERVAL = 3000;
const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
const { closeGiftUpgradeModal, closeGiftInfoModal, upgradeGift, upgradePrepaidGift } = getActions();
const {
closeGiftUpgradeModal,
closeGiftInfoModal,
upgradeGift,
upgradePrepaidGift,
openStarGiftPriceDecreaseInfoModal,
shiftGiftUpgradeNextPrice,
} = getActions();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
@ -64,15 +73,20 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
const handleClose = useLastCallback(() => closeGiftUpgradeModal());
const handleTimerEnd = useLastCallback(() => {
shiftGiftUpgradeNextPrice();
});
const nextPrice = renderingModal?.nextPrices?.[0];
const nextPriceDate = nextPrice?.date;
const upgradeStars = renderingModal?.currentUpgradeStars;
const handleUpgrade = useLastCallback(() => {
const gift = renderingModal?.gift;
if (!gift) return;
const regularGift = gift.gift.type === 'starGift' ? gift.gift : undefined;
if (isPrepaid && gift.prepaidUpgradeHash && renderingRecipient) {
const upgradeStars = regularGift?.upgradeStars;
if (!upgradeStars) return;
upgradePrepaidGift({
@ -90,7 +104,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
upgradeGift({
gift: gift.inputGift,
shouldKeepOriginalDetails,
upgradeStars: !gift.alreadyPaidUpgradeStars ? regularGift?.upgradeStars : undefined,
upgradeStars: !gift.alreadyPaidUpgradeStars ? upgradeStars : undefined,
});
handleClose();
});
@ -100,6 +114,17 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
setPreviewAttributes(getRandomAttributes(renderingModal.sampleAttributes, previewAttributes));
});
const handleOpenPriceInfo = useLastCallback(() => {
if (!renderingModal?.prices) return;
openStarGiftPriceDecreaseInfoModal({
prices: renderingModal.prices,
currentPrice: upgradeStars || 0,
minPrice: renderingModal.minPrice || 0,
maxPrice: renderingModal.maxPrice || 0,
});
});
useInterval(updatePreviewAttributes, isOpen ? PREVIEW_UPDATE_INTERVAL : undefined, true);
useEffect(() => {
@ -123,6 +148,13 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
});
}, [renderingModal?.sampleAttributes]);
const formattedPriceElement = useMemo(() => (upgradeStars ? (
<span>
<Icon name="star" className="star-amount-icon" />
<AnimatedCounter text={lang.number(upgradeStars)} />
</span>
) : undefined), [lang, upgradeStars]);
const modalData = useMemo(() => {
if (!previewAttributes || !isOpen) {
return undefined;
@ -147,9 +179,9 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
? lang('GiftPeerUpgradeText', { peer: getPeerTitle(lang, renderingRecipient) })
: lang('GiftUpgradeTextOwn');
const formattedPrice = gift
? formatStarsAsIcon(lang, (gift.gift as ApiStarGiftRegular).upgradeStars!, { asFont: true })
: undefined;
const hasPriceDecreaseInfo = Boolean(nextPriceDate)
&& Boolean(renderingModal?.prices?.length)
&& !gift?.alreadyPaidUpgradeStars;
const header = (
<UniqueGiftHeader
@ -178,12 +210,32 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
/>
)}
<Button className={styles.footerButton} isShiny onClick={handleUpgrade}>
{gift.alreadyPaidUpgradeStars
? lang('GeneralConfirm')
: isPrepaid
? lang('GiftPayForUpgradeButton', { amount: formattedPrice }, { withNodes: true })
: lang('GiftUpgradeButton', { amount: formattedPrice }, { withNodes: true })}
<div className={styles.buttonContent}>
<div>
{gift.alreadyPaidUpgradeStars
? lang('GeneralConfirm')
: isPrepaid
? lang('GiftPayForUpgradeButton', { amount: formattedPriceElement }, { withNodes: true })
: lang('GiftUpgradeButton', { amount: formattedPriceElement }, { withNodes: true })}
</div>
{hasPriceDecreaseInfo && (
<div className={styles.priceDecreaseTimer}>
{lang('StarGiftPriceDecreaseTimer', {
timer: <TextTimer endsAt={nextPriceDate} onEnd={handleTimerEnd} />,
}, { withNodes: true })}
</div>
)}
</div>
</Button>
{hasPriceDecreaseInfo && (
<Link
className={styles.link}
isPrimary
onClick={handleOpenPriceInfo}
>
{lang('StarGiftPriceDecreaseInfoLink')}
</Link>
)}
</>
)}
</div>
@ -196,7 +248,9 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
};
}, [previewAttributes, isOpen, lang,
renderingRecipient, renderingModal?.gift,
shouldKeepOriginalDetails, isPrepaid]);
shouldKeepOriginalDetails, isPrepaid,
renderingModal?.prices?.length,
nextPriceDate, formattedPriceElement]);
return (
<TableAboutModal

View File

@ -8,6 +8,7 @@ import {
} from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByCallback, buildCollectionByKey } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import { RESALE_GIFTS_LIMIT } from '../../../limits';
import { areInputSavedGiftsEqual, getRequestInputSavedStarGift } from '../../helpers/payments';
@ -494,11 +495,24 @@ addActionHandler('openGiftUpgradeModal', async (global, actions, payload): Promi
giftId, gift, peerId, tabId = getCurrentTabId(),
} = payload;
const samples = await callApi('fetchStarGiftUpgradePreview', {
const preview = await callApi('fetchStarGiftUpgradePreview', {
giftId,
});
if (!samples) return;
if (!preview) return;
const serverTime = getServerTime();
const filteredPrices = preview.prices.filter((price) => price.date > serverTime);
const filteredNextPrices = preview.nextPrices.filter((price) => price.date > serverTime);
const passedPrices = preview.nextPrices.filter((price) => price.date <= serverTime);
const regularGift = gift?.gift.type === 'starGift' ? gift.gift : undefined;
const currentUpgradeStars = passedPrices.length
? passedPrices[passedPrices.length - 1].upgradeStars
: regularGift?.upgradeStars;
const maxPrice = preview.prices[0]?.upgradeStars;
const minPrice = preview.prices.at(-1)?.upgradeStars;
global = getGlobal();
@ -506,13 +520,65 @@ addActionHandler('openGiftUpgradeModal', async (global, actions, payload): Promi
giftUpgradeModal: {
recipientId: peerId,
gift,
sampleAttributes: samples,
sampleAttributes: preview.sampleAttributes,
prices: filteredPrices,
nextPrices: filteredNextPrices,
currentUpgradeStars,
minPrice,
maxPrice,
},
}, tabId);
setGlobal(global);
});
addActionHandler('shiftGiftUpgradeNextPrice', async (global, _actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
const giftUpgradeModal = tabState?.giftUpgradeModal;
if (!giftUpgradeModal?.nextPrices?.length) return;
const currentUpgradeStars = giftUpgradeModal.nextPrices[0].upgradeStars;
const newNextPrices = giftUpgradeModal.nextPrices.slice(1);
if (newNextPrices.length) {
global = updateTabState(global, {
giftUpgradeModal: {
...giftUpgradeModal,
nextPrices: newNextPrices,
currentUpgradeStars,
},
}, tabId);
setGlobal(global);
return;
}
const gift = giftUpgradeModal.gift?.gift;
const giftId = gift?.type === 'starGift' ? gift.id : undefined;
if (!giftId) return;
const preview = await callApi('fetchStarGiftUpgradePreview', { giftId });
if (!preview) return;
const serverTime = getServerTime();
const filteredNextPrices = preview.nextPrices.filter((price) => price.date > serverTime);
global = getGlobal();
const currentTabState = selectTabState(global, tabId);
const currentModal = currentTabState?.giftUpgradeModal;
if (!currentModal) return;
global = updateTabState(global, {
giftUpgradeModal: {
...currentModal,
nextPrices: filteredNextPrices,
currentUpgradeStars,
},
}, tabId);
setGlobal(global);
});
addActionHandler('toggleSavedGiftPinned', async (global, actions, payload): Promise<void> => {
const { gift, peerId, tabId = getCurrentTabId() } = payload;

View File

@ -387,6 +387,23 @@ addTabStateResetterAction('closeGiftResalePriceComposerModal', 'giftResalePriceC
addTabStateResetterAction('closeGiftUpgradeModal', 'giftUpgradeModal');
addActionHandler('openStarGiftPriceDecreaseInfoModal', (global, actions, payload): ActionReturnType => {
const {
prices, currentPrice, minPrice, maxPrice, tabId = getCurrentTabId(),
} = payload;
return updateTabState(global, {
starGiftPriceDecreaseInfoModal: {
prices,
currentPrice,
minPrice,
maxPrice,
},
}, tabId);
});
addTabStateResetterAction('closeStarGiftPriceDecreaseInfoModal', 'starGiftPriceDecreaseInfoModal');
addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionReturnType => {
const { gift, tabId = getCurrentTabId() } = payload || {};

View File

@ -46,6 +46,7 @@ import type {
ApiStarGift,
ApiStarGiftAttributeOriginalDetails,
ApiStarGiftUnique,
ApiStarGiftUpgradePrice,
ApiStarsSubscription,
ApiStarsTransaction,
ApiSticker,
@ -2628,6 +2629,15 @@ export interface ActionPayloads {
gift?: ApiSavedStarGift;
} & WithTabId;
closeGiftUpgradeModal: WithTabId | undefined;
shiftGiftUpgradeNextPrice: WithTabId | undefined;
openStarGiftPriceDecreaseInfoModal: {
prices: ApiStarGiftUpgradePrice[];
currentPrice: number;
minPrice: number;
maxPrice: number;
} & WithTabId;
closeStarGiftPriceDecreaseInfoModal: WithTabId | undefined;
upgradeGift: {
gift: ApiInputSavedStarGift;
shouldKeepOriginalDetails?: boolean;

View File

@ -44,6 +44,7 @@ import type {
ApiStarGiftAttributeCounter,
ApiStarGiftAttributeOriginalDetails,
ApiStarGiftUnique,
ApiStarGiftUpgradePrice,
ApiStarGiveawayOption,
ApiStarsSubscription,
ApiStarsTransaction,
@ -867,6 +868,11 @@ export type TabState = {
sampleAttributes: ApiStarGiftAttribute[];
recipientId?: string;
gift?: ApiSavedStarGift;
prices?: ApiStarGiftUpgradePrice[];
nextPrices?: ApiStarGiftUpgradePrice[];
currentUpgradeStars?: number;
minPrice?: number;
maxPrice?: number;
};
giftWithdrawModal?: {
@ -879,6 +885,13 @@ export type TabState = {
emojiStatus: ApiEmojiStatusCollectible;
};
starGiftPriceDecreaseInfoModal?: {
prices: ApiStarGiftUpgradePrice[];
currentPrice: number;
minPrice: number;
maxPrice: number;
};
suggestedStatusModal?: {
botId: string;
webAppKey?: string;

View File

@ -1782,6 +1782,11 @@ export interface LangPair {
'StealthModeButtonPremium': undefined;
'StealthModeButton': undefined;
'StealthModeButtonToStory': undefined;
'UsersWhoUpgradeFirst': undefined;
'UpgradeCostDrops': undefined;
'StarGiftPriceDecreaseInfoLink': undefined;
'StarGiftUpgradeCostModalTitle': undefined;
'StarGiftUpgradeCostHint': undefined;
}
export interface LangPairWithVariables<V = LangVariable> {
@ -3073,6 +3078,9 @@ export interface LangPairWithVariables<V = LangVariable> {
'StealthModeComposerPlaceholder': {
'timer': V;
};
'StarGiftPriceDecreaseTimer': {
'timer': V;
};
}
export interface LangPairPlural {

View File

@ -398,13 +398,13 @@ export function formatDateToString(
export function formatDateTimeToString(
datetime: Date | number, locale = 'en-US', noSeconds?: boolean,
timeFormat?: TimeFormat,
timeFormat?: TimeFormat, noYear?: boolean,
) {
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
return date.toLocaleString(
locale,
{
year: 'numeric',
year: noYear ? undefined : 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',