From aacd4a8429598e0fe5690bf7a122359435ab8571 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 11 Oct 2025 19:07:20 +0200 Subject: [PATCH] Gift Modal: Support `My gifts` category (#6316) --- src/assets/localization/fallback.strings | 7 +- src/bundles/stars.ts | 1 + src/components/main/Main.tsx | 2 + src/components/modals/ModalContainer.tsx | 3 + .../modals/gift/GiftItem.module.scss | 5 + src/components/modals/gift/GiftItemStar.tsx | 34 ++++- .../modals/gift/GiftModal.module.scss | 2 +- src/components/modals/gift/GiftModal.tsx | 123 ++++++++++++++---- .../gift/StarGiftCategoryList.module.scss | 2 +- .../modals/gift/StarGiftCategoryList.tsx | 35 ++--- .../gift/info/GiftInfoModal.module.scss | 11 ++ .../modals/gift/info/GiftInfoModal.tsx | 9 ++ .../GiftTransferConfirmModal.async.tsx | 20 +++ .../transfer/GiftTransferConfirmModal.tsx | 108 +++++++++++++++ .../gift/transfer/GiftTransferModal.tsx | 90 ++----------- src/global/actions/api/stars.ts | 96 +++++++++----- src/global/actions/apiUpdaters/misc.ts | 2 + src/global/actions/apiUpdaters/payments.ts | 1 + src/global/actions/ui/stars.ts | 15 +++ src/global/types/actions.ts | 8 ++ src/global/types/globalState.ts | 6 + src/global/types/tabState.ts | 5 + src/types/index.ts | 2 +- src/types/language.d.ts | 7 +- 24 files changed, 419 insertions(+), 175 deletions(-) create mode 100644 src/components/modals/gift/transfer/GiftTransferConfirmModal.async.tsx create mode 100644 src/components/modals/gift/transfer/GiftTransferConfirmModal.tsx diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 642b654e1..5cc627ab0 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1452,6 +1452,7 @@ "StarsGiftHeader" = "Send a Gift"; "StarsGiftHeaderSelf" = "Buy a Gift"; "StarGiftDescription" = "Give {user} gifts that can be kept on the profile or converted to Stars."; +"StarGiftDescriptionCollectibles" = "Collectible gifts are unique digital items you exchange or sell."; "StarGiftDescriptionSelf" = "Buy yourself a gift to add to your profile or reserve for later.\n\nLimited-edition gifts upgraded to collectibles can be gifted to others."; "StarGiftDescriptionChannel" = "Select gift to show appreciation to **{peer}**."; "GiftLimited" = "limited"; @@ -1583,8 +1584,6 @@ "StarsAmountText_one" = "{amount} Star"; "StarsAmountText_other" = "{amount} Stars"; "AllGiftsCategory" = "All gifts"; -"LimitedGiftsCategory" = "Limited"; -"StockGiftsCategory" = "In Stock"; "PremiumGiftDescription" = "Premium"; "SendPaidReaction" = "Send ⭐️{amount}"; "StarsPay" = "Confirm and Pay {amount}"; @@ -2070,7 +2069,8 @@ "ComposerTitleForwardFrom" = "From: **{users}**"; "ContextMenuItemMention" = "Mention"; "GiftRibbonResale" = "resale"; -"GiftCategoryResale" = "Resale"; +"GiftCategoryCollectibles" = "Collectibles"; +"GiftCategoryMyGifts" = "My Gifts"; "HeaderDescriptionResaleGifts_one" = "{count} for resale"; "HeaderDescriptionResaleGifts_other" = "{count} for resale"; "GiftSortByPrice" = "Sort by Price"; @@ -2285,6 +2285,7 @@ "GiftValueForSaleOnFragment" = "for sale on Fragment"; "GiftValueForSaleOnTelegram" = "for sale on Telegram"; "EmbeddedMessageNoCaption" = "Caption removed"; +"ConfirmBuyGiftForTonDescription" = "The seller only accepts TON as payment."; "TitleGiftLocked" = "Gift Locked"; "GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**."; "QuickPreview" = "Quick Preview"; diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index 74c01cca3..2e0b11c44 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -15,5 +15,6 @@ export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/G 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'; +export { default as GiftTransferConfirmModal } from '../components/modals/gift/transfer/GiftTransferConfirmModal'; export { default as ChatRefundModal } from '../components/modals/stars/chatRefund/ChatRefundModal'; export { default as PriceConfirmModal } from '../components/modals/priceConfirm/PriceConfirmModal'; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 300e34ea9..a818eb6a2 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -217,6 +217,7 @@ const Main = ({ loadPremiumGifts, loadTonGifts, loadStarGifts, + loadMyCollectibleGifts, loadDefaultTopicIcons, loadAddedStickers, loadFavoriteStickers, @@ -332,6 +333,7 @@ const Main = ({ loadPremiumGifts(); loadTonGifts(); loadStarGifts(); + loadMyCollectibleGifts(); loadAvailableEffects(); loadBirthdayNumbersStickers(); loadRestrictedEmojiStickers(); diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 4b791a7c0..ceb182f0c 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -25,6 +25,7 @@ import GiftLockedModal from './gift/locked/GiftLockedModal.async'; import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async'; import GiftResalePriceComposerModal from './gift/resale/GiftResalePriceComposerModal.async'; import GiftStatusInfoModal from './gift/status/GiftStatusInfoModal.async'; +import GiftTransferConfirmModal from './gift/transfer/GiftTransferConfirmModal.async'; import GiftTransferModal from './gift/transfer/GiftTransferModal.async'; import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async'; import GiftInfoValueModal from './gift/value/GiftInfoValueModal.async'; @@ -95,6 +96,7 @@ type ModalKey = keyof Pick void; isResale?: boolean; + withTransferBadge?: boolean; }; type StateProps = { @@ -38,7 +39,7 @@ type StateProps = { const GIFT_STICKER_SIZE = 90; function GiftItemStar({ - gift, observeIntersection, onClick, isResale, isCurrentUserPremium, + gift, observeIntersection, onClick, isResale, isCurrentUserPremium, withTransferBadge, }: OwnProps & StateProps) { const { openGiftInfoModal, openPremiumModal, showNotification, checkCanSendGift } = getActions(); @@ -72,7 +73,7 @@ function GiftItemStar({ : getPriceAmount(uniqueGift?.resellPrice); const priceCurrency = priceInfo?.currency || STARS_CURRENCY_CODE; const resellMinStars = regularGift?.resellMinStars; - const priceInStarsAsString = !isGiftUnique && isResale && resellMinStars + const formattedPrice = !isGiftUnique && isResale && resellMinStars ? lang.number(resellMinStars) + '+' : priceInfo?.amount || 0; const isLimited = !isGiftUnique && Boolean(regularGift?.isLimited); const isSoldOut = !isGiftUnique && Boolean(regularGift?.isSoldOut); @@ -175,6 +176,24 @@ function GiftItemStar({ setIsVisible(visible); }); + const badgeContent = useMemo(() => { + if (withTransferBadge) { + return lang('GiftTransferTitle'); + } + + if (priceCurrency === TON_CURRENCY_CODE) { + return formatTonAsIcon(lang, formattedPrice || 0, { + shouldConvertFromNanos: true, + className: styles.star, + }); + } + + return formatStarsAsIcon(lang, formattedPrice || 0, { + asFont: true, + className: styles.star, + }); + }, [withTransferBadge, priceCurrency, formattedPrice, lang]); + return (
{giftRibbon} {isLocked && } diff --git a/src/components/modals/gift/GiftModal.module.scss b/src/components/modals/gift/GiftModal.module.scss index f57744069..73c2b75c7 100644 --- a/src/components/modals/gift/GiftModal.module.scss +++ b/src/components/modals/gift/GiftModal.module.scss @@ -81,7 +81,7 @@ .starGiftsContainer, .premiumGiftsGallery { display: flex; - gap: 0.625rem; + gap: 0.5rem; align-items: center; justify-content: center; diff --git a/src/components/modals/gift/GiftModal.tsx b/src/components/modals/gift/GiftModal.tsx index 49994d56c..672f8e76c 100644 --- a/src/components/modals/gift/GiftModal.tsx +++ b/src/components/modals/gift/GiftModal.tsx @@ -9,6 +9,7 @@ import type { ApiDisallowedGifts, ApiPeer, ApiPremiumGiftCodeOption, + ApiSavedStarGift, ApiStarGift, ApiStarGiftRegular, ApiStarsAmount, @@ -34,6 +35,7 @@ import Avatar from '../../common/Avatar'; import InteractiveSparkles from '../../common/InteractiveSparkles'; import SafeLink from '../../common/SafeLink'; import Button from '../../ui/Button'; +import InfiniteScroll from '../../ui/InfiniteScroll'; import Modal from '../../ui/Modal'; import Transition from '../../ui/Transition'; import BalanceBlock from '../stars/BalanceBlock'; @@ -56,6 +58,8 @@ type StateProps = { boostPerSentGift?: number; starGiftsById?: Record; starGiftIdsByCategory?: Record; + myCollectibleGiftsById?: Record; + myCollectibleGiftIds?: string[]; starBalance?: ApiStarsAmount; peer?: ApiPeer; isSelf?: boolean; @@ -77,6 +81,8 @@ const GiftModal: FC = ({ modal, starGiftsById, starGiftIdsByCategory, + myCollectibleGiftsById, + myCollectibleGiftIds, starBalance, peer, isSelf, @@ -87,7 +93,14 @@ const GiftModal: FC = ({ tabId, }) => { const { - closeGiftModal, openGiftInfoModal, resetResaleGifts, loadResaleGifts, openGiftInMarket, closeResaleGiftsMarket, + closeGiftModal, + openGiftInfoModal, + resetResaleGifts, + loadResaleGifts, + openGiftInMarket, + closeResaleGiftsMarket, + loadMyCollectibleGifts, + openGiftTransferConfirmModal, } = getActions(); const dialogRef = useRef(); const transitionRef = useRef(); @@ -122,6 +135,7 @@ const GiftModal: FC = ({ const areUnlimitedStarGiftsDisallowed = !isSelf && disallowedGifts?.shouldDisallowUnlimitedStarGifts; const areLimitedStarGiftsDisallowed = !isSelf && disallowedGifts?.shouldDisallowLimitedStarGifts; + const areUniqueStarGiftsDisallowed = !isSelf && disallowedGifts?.shouldDisallowUniqueStarGifts; const oldLang = useOldLang(); const lang = useLang(); @@ -206,19 +220,29 @@ const GiftModal: FC = ({ ), }, { withNodes: true }); - const starGiftDescription = chat - ? lang('StarGiftDescriptionChannel', { peer: getPeerTitle(lang, chat) }, { - withNodes: true, - withMarkdown: true, - }) - : isSelf - ? lang('StarGiftDescriptionSelf', undefined, { + const starGiftDescription = useMemo(() => { + if (chat) { + return lang('StarGiftDescriptionChannel', { peer: getPeerTitle(lang, chat) }, { + withNodes: true, + withMarkdown: true, + }); + } + + if (isSelf) { + return lang('StarGiftDescriptionSelf', undefined, { withNodes: true, renderTextFilters: ['br'], - }) - : lang('StarGiftDescription', { - user: getUserFullName(user)!, - }, { withNodes: true, withMarkdown: true }); + }); + } + + if (selectedCategory === 'resale') { + return lang('StarGiftDescriptionCollectibles'); + } + + return lang('StarGiftDescription', { + user: getUserFullName(user)!, + }, { withNodes: true, withMarkdown: true }); + }, [chat, isSelf, selectedCategory, user, lang]); function renderGiftPremiumHeader() { return ( @@ -268,22 +292,62 @@ const GiftModal: FC = ({ setIsGiftScreenHeaderForStarGifts('id' in gift); }); + const handleMyGiftClick = useLastCallback((gift: ApiStarGift) => { + if (gift.type === 'starGift' || !myCollectibleGiftsById || !peer?.id) return; + const savedGift = myCollectibleGiftsById[gift.id]; + + openGiftTransferConfirmModal({ + gift: savedGift, + recipientId: peer.id, + }); + }); + + const handleLoadMore = useLastCallback(() => { + if (selectedCategory === 'myCollectibles') { + loadMyCollectibleGifts(); + } + }); + function renderStarGifts() { + if (selectedCategory === 'myCollectibles') { + return ( + + {myCollectibleGiftsById && myCollectibleGiftIds?.map((giftId) => { + const savedGift = myCollectibleGiftsById[giftId]; + if (!savedGift) return undefined; + + return ( + + ); + })} + + ); + } + const filteredGiftIds = starGiftIdsByCategory?.[selectedCategory]?.filter((giftId) => { const gift = starGiftsById?.[giftId]; if (!gift) return false; - const { isLimited, isSoldOut, upgradeStars } = gift; - if (areUnlimitedStarGiftsDisallowed && !areLimitedStarGiftsDisallowed) { - return isLimited; - } - if (areLimitedStarGiftsDisallowed && !areUnlimitedStarGiftsDisallowed) { - return !isLimited && !isSoldOut; - } - if (areUnlimitedStarGiftsDisallowed && areLimitedStarGiftsDisallowed) { - return Boolean(isLimited && Boolean(upgradeStars)); + const { isLimited, availabilityResale } = gift; + + if (areLimitedStarGiftsDisallowed && isLimited) { + return !areUniqueStarGiftsDisallowed ? availabilityResale : false; } + if (areUnlimitedStarGiftsDisallowed && !isLimited) return false; + return true; }); @@ -291,8 +355,9 @@ const GiftModal: FC = ({
{starGiftsById && filteredGiftIds?.flatMap((giftId) => { const gift = starGiftsById[giftId]; - const shouldShowResale = selectedCategory !== 'stock' && Boolean(gift.availabilityResale); - const shouldDuplicateAsResale = selectedCategory !== 'resale' && shouldShowResale && !gift.isSoldOut; + const shouldShowResale = Boolean(gift.availabilityResale) && !areUniqueStarGiftsDisallowed; + const shouldDuplicateAsResale = selectedCategory !== 'resale' + && shouldShowResale && !gift.isSoldOut && !areLimitedStarGiftsDisallowed; const elements = [ = ({ {renderStarGiftsHeader()} {renderStarGiftsDescription()} ((global, { modal }): Complete((global, { modal }): Complete void; }; @@ -26,16 +26,13 @@ type StateProps = { const StarGiftCategoryList = ({ idsByCategory, onCategoryChanged, - areLimitedStarGiftsDisallowed, + areCollectibleStarGiftsDisallowed, + isSelf, + hasCollectible, }: StateProps & OwnProps) => { const ref = useRef(); const lang = useLang(); - const starCategories: number[] | undefined = useMemo(() => idsByCategory && Object.keys(idsByCategory) - .filter((category) => category !== 'all' && category !== 'limited') - .map(Number) - .sort((a, b) => a - b), - [idsByCategory]); const hasResale = idsByCategory && idsByCategory['resale'].length > 0; @@ -50,9 +47,8 @@ const StarGiftCategoryList = ({ function renderCategoryName(category: StarGiftCategory) { if (category === 'all') return lang('AllGiftsCategory'); - if (category === 'stock') return lang('StockGiftsCategory'); - if (category === 'limited') return lang('LimitedGiftsCategory'); - if (category === 'resale') return lang('GiftCategoryResale'); + if (category === 'myCollectibles') return lang('GiftCategoryMyGifts'); + if (category === 'resale') return lang('GiftCategoryCollectibles'); return category; } @@ -65,13 +61,6 @@ const StarGiftCategoryList = ({ )} onClick={() => handleItemClick(category)} > - {Number.isInteger(category) && ( - - )} {renderCategoryName(category)}
); @@ -82,10 +71,8 @@ const StarGiftCategoryList = ({ return (
{renderCategoryItem('all')} - {!areLimitedStarGiftsDisallowed && renderCategoryItem('limited')} - {!areLimitedStarGiftsDisallowed && hasResale && renderCategoryItem('resale')} - {renderCategoryItem('stock')} - {starCategories?.map(renderCategoryItem)} + {!areCollectibleStarGiftsDisallowed && !isSelf && hasCollectible && renderCategoryItem('myCollectibles')} + {!areCollectibleStarGiftsDisallowed && hasResale && renderCategoryItem('resale')}
); }; diff --git a/src/components/modals/gift/info/GiftInfoModal.module.scss b/src/components/modals/gift/info/GiftInfoModal.module.scss index 1921f075e..95e7fdd1a 100644 --- a/src/components/modals/gift/info/GiftInfoModal.module.scss +++ b/src/components/modals/gift/info/GiftInfoModal.module.scss @@ -19,6 +19,17 @@ font-size: 1.25rem; } +.descriptionConfirm { + padding-bottom: 0.75rem; + color: var(--color-text-secondary); +} + +.titleConfirm { + margin-bottom: 0.75rem; + font-size: 1.25rem; + font-weight: var(--font-weight-medium); +} + .header { display: flex; flex-direction: column; diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index d9083c900..076fce12e 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -938,10 +938,19 @@ const GiftInfoModal = ({ confirmHandler={handleConfirmBuyGift} > + {uniqueGift.resaleTonOnly + && ( +
+ {lang('ConfirmBuyGiftForTonDescription')} +
+ )} +
+ {lang('TitleConfirmPayment')} +
{!recipientPeer && (

diff --git a/src/components/modals/gift/transfer/GiftTransferConfirmModal.async.tsx b/src/components/modals/gift/transfer/GiftTransferConfirmModal.async.tsx new file mode 100644 index 000000000..1a8be517e --- /dev/null +++ b/src/components/modals/gift/transfer/GiftTransferConfirmModal.async.tsx @@ -0,0 +1,20 @@ +import type { FC } from '../../../../lib/teact/teact'; + +import type { OwnProps } from './GiftTransferConfirmModal'; + +import { Bundles } from '../../../../util/moduleLoader'; + +import useModuleLoader from '../../../../hooks/useModuleLoader'; + +const GiftTransferConfirmModalAsync: FC = (props) => { + const { modal } = props; + const GiftTransferConfirmModal = useModuleLoader( + Bundles.Stars, + 'GiftTransferConfirmModal', + !modal, + ); + + return GiftTransferConfirmModal ? : undefined; +}; + +export default GiftTransferConfirmModalAsync; diff --git a/src/components/modals/gift/transfer/GiftTransferConfirmModal.tsx b/src/components/modals/gift/transfer/GiftTransferConfirmModal.tsx new file mode 100644 index 000000000..b4e854e38 --- /dev/null +++ b/src/components/modals/gift/transfer/GiftTransferConfirmModal.tsx @@ -0,0 +1,108 @@ +import { memo } from '../../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../../global'; + +import type { ApiPeer } from '../../../../api/types'; +import type { TabState } from '../../../../global/types'; + +import { getPeerTitle } from '../../../../global/helpers/peers'; +import { selectPeer } from '../../../../global/selectors'; +import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format'; + +import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; +import useLang from '../../../../hooks/useLang'; +import useLastCallback from '../../../../hooks/useLastCallback'; + +import GiftTransferPreview from '../../../common/gift/GiftTransferPreview'; +import ConfirmDialog from '../../../ui/ConfirmDialog'; + +export type OwnProps = { + modal: TabState['giftTransferConfirmModal']; +}; + +type StateProps = { + selectedPeer?: ApiPeer; +}; + +const GiftTransferConfirmModal = ({ modal, selectedPeer }: OwnProps & StateProps) => { + const { + closeGiftTransferConfirmModal, transferGift, openChat, closeGiftModal, closeGiftTransferModal, + } = getActions(); + const lang = useLang(); + + const isOpen = Boolean(modal); + const renderingModal = useCurrentOrPrev(modal); + const renderingSelectedPeer = useCurrentOrPrev(selectedPeer); + + const handleConfirm = useLastCallback(() => { + if (!renderingModal?.gift.inputGift || !renderingModal.recipientId) return; + + transferGift({ + gift: renderingModal.gift.inputGift, + recipientId: renderingModal.recipientId, + transferStars: renderingModal.gift.transferStars, + }); + + closeGiftTransferConfirmModal(); + openChat({ id: renderingModal.recipientId }); + + closeGiftModal(); + closeGiftTransferModal(); + }); + + if (!renderingModal) return undefined; + + const { gift } = renderingModal; + const uniqueGift = gift.gift.type === 'starGiftUnique' ? gift.gift : undefined; + + if (!uniqueGift) return undefined; + + return ( + + {renderingSelectedPeer && ( + + )} +

+ {gift.transferStars + ? lang('GiftTransferConfirmDescription', { + gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }), + amount: formatStarsAsText(lang, gift.transferStars), + peer: getPeerTitle(lang, renderingSelectedPeer!), + }, { + withNodes: true, + withMarkdown: true, + }) + : lang('GiftTransferConfirmDescriptionFree', { + gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }), + peer: getPeerTitle(lang, renderingSelectedPeer!), + }, { + withNodes: true, + withMarkdown: true, + })} +

+ + ); +}; + +export default memo( + withGlobal((global, { modal }): Complete => { + const selectedPeer = modal?.recipientId ? selectPeer(global, modal.recipientId) : undefined; + + return { + selectedPeer, + }; + })(GiftTransferConfirmModal), +); diff --git a/src/components/modals/gift/transfer/GiftTransferModal.tsx b/src/components/modals/gift/transfer/GiftTransferModal.tsx index de8cac85b..3d3296d0e 100644 --- a/src/components/modals/gift/transfer/GiftTransferModal.tsx +++ b/src/components/modals/gift/transfer/GiftTransferModal.tsx @@ -1,19 +1,15 @@ import { - memo, useEffect, useMemo, useState, + memo, useMemo, useState, } from '../../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../../global'; -import type { ApiStarGiftUnique } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; import type { UniqueCustomPeer } from '../../../../types'; import { ALL_FOLDER_ID } from '../../../../config'; -import { getPeerTitle } from '../../../../global/helpers/peers'; -import { selectCanGift, selectPeer } from '../../../../global/selectors'; +import { selectCanGift } from '../../../../global/selectors'; import { unique } from '../../../../util/iteratees'; -import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format'; import { MEMO_EMPTY_ARRAY } from '../../../../util/memo'; -import { getGiftAttributes } from '../../../common/helpers/gifts'; import sortChatIds from '../../../common/helpers/sortChatIds'; import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; @@ -22,10 +18,8 @@ import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; import usePeerSearch from '../../../../hooks/usePeerSearch'; -import GiftTransferPreview from '../../../common/gift/GiftTransferPreview'; import PeerPicker from '../../../common/pickers/PeerPicker'; import PickerModal from '../../../common/pickers/PickerModal'; -import ConfirmDialog from '../../../ui/ConfirmDialog'; export type OwnProps = { modal: TabState['giftTransferModal']; @@ -41,7 +35,11 @@ type Categories = 'withdraw'; const GiftTransferModal = ({ modal, contactIds, currentUserId, }: OwnProps & StateProps) => { - const { closeGiftTransferModal, openGiftWithdrawModal, transferGift } = getActions(); + const { + closeGiftTransferModal, + openGiftWithdrawModal, + openGiftTransferConfirmModal, + } = getActions(); const isOpen = Boolean(modal); const lang = useLang(); @@ -49,16 +47,6 @@ const GiftTransferModal = ({ const [searchQuery, setSearchQuery] = useState(''); const renderingModal = useCurrentOrPrev(modal); - const uniqueGift = renderingModal?.gift?.gift as ApiStarGiftUnique; - const giftAttributes = uniqueGift && getGiftAttributes(uniqueGift); - - const [selectedId, setSelectedId] = useState(); - - const renderingSelectedPeerId = useCurrentOrPrev(selectedId); - const renderingSelectedPeer = useMemo(() => { - const global = getGlobal(); - return renderingSelectedPeerId ? selectPeer(global, renderingSelectedPeerId) : undefined; - }, [renderingSelectedPeerId]); const orderedChatIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID); @@ -107,26 +95,13 @@ const GiftTransferModal = ({ false); }, [isLoading, foundIds, currentUserId]); - const closeConfirmModal = useLastCallback(() => { - setSelectedId(undefined); - }); + const handlePeerSelect = useLastCallback((peerId: string) => { + if (!renderingModal?.gift) return; - useEffect(() => { - if (!isOpen) { - setSelectedId(undefined); - } - }, [isOpen]); - - const handleTransfer = useLastCallback(() => { - if (!renderingModal?.gift.inputGift) return; - transferGift({ - gift: renderingModal.gift.inputGift, - recipientId: renderingSelectedPeerId!, - transferStars: renderingModal.gift.transferStars, + openGiftTransferConfirmModal({ + gift: renderingModal.gift, + recipientId: peerId, }); - - closeConfirmModal(); - closeGiftTransferModal(); }); return ( @@ -151,47 +126,8 @@ const GiftTransferModal = ({ filterValue={searchQuery} filterPlaceholder={lang('Search')} onFilterChange={setSearchQuery} - onSelectedIdChange={setSelectedId} + onSelectedIdChange={handlePeerSelect} /> - {giftAttributes && ( - - {renderingSelectedPeer && ( - - )} -

- {renderingModal?.gift.transferStars - ? lang('GiftTransferConfirmDescription', { - gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }), - amount: formatStarsAsText(lang, renderingModal.gift.transferStars), - peer: getPeerTitle(lang, renderingSelectedPeer!), - }, { - withNodes: true, - withMarkdown: true, - }) - : lang('GiftTransferConfirmDescriptionFree', { - gift: lang('GiftUnique', { title: uniqueGift.title, number: uniqueGift.number }), - peer: getPeerTitle(lang, renderingSelectedPeer!), - }, { - withNodes: true, - withMarkdown: true, - })} -

-
- )} ); }; diff --git a/src/global/actions/api/stars.ts b/src/global/actions/api/stars.ts index f01fafa50..45ce914a0 100644 --- a/src/global/actions/api/stars.ts +++ b/src/global/actions/api/stars.ts @@ -1,5 +1,4 @@ import type { ApiSavedStarGift, ApiStarGiftUnique } from '../../../api/types'; -import type { StarGiftCategory } from '../../../types'; import type { ActionReturnType } from '../../types'; import { @@ -8,7 +7,7 @@ import { TON_CURRENCY_CODE, } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; -import { buildCollectionByKey } from '../../../util/iteratees'; +import { buildCollectionByCallback, buildCollectionByKey } from '../../../util/iteratees'; import { callApi } from '../../../api/gramjs'; import { RESALE_GIFTS_LIMIT } from '../../../limits'; import { areInputSavedGiftsEqual, getRequestInputSavedStarGift } from '../../helpers/payments'; @@ -136,49 +135,83 @@ addActionHandler('loadStarGifts', async (global): Promise => { const byId = buildCollectionByKey(result.gifts, 'id'); - const idsByCategoryName: Record = { - all: [], - stock: [], - limited: [], - resale: [], - }; - const allStarGiftIds = Object.keys(byId); const allStarGifts = Object.values(byId); - const limitedStarGiftIds = allStarGifts.map((gift) => (gift.isLimited ? gift.id : undefined)) - .filter(Boolean); - - const stockedStarGiftIds = allStarGifts.map((gift) => ( - gift.availabilityRemains || !gift.availabilityTotal ? gift.id : undefined - )).filter(Boolean); - const resaleStarGiftIds = allStarGifts.map((gift) => (gift.availabilityResale ? gift.id : undefined)) .filter(Boolean); - idsByCategoryName.all = allStarGiftIds; - idsByCategoryName.limited = limitedStarGiftIds; - idsByCategoryName.stock = stockedStarGiftIds; - idsByCategoryName.resale = resaleStarGiftIds; - - allStarGifts.forEach((gift) => { - const starsCategory = gift.stars; - if (!idsByCategoryName[starsCategory]) { - idsByCategoryName[starsCategory] = []; - } - idsByCategoryName[starsCategory].push(gift.id); - }); - global = { ...global, starGifts: { byId, - idsByCategory: idsByCategoryName, + idsByCategory: { + all: allStarGiftIds, + resale: resaleStarGiftIds, + myCollectibles: [], + }, }, }; setGlobal(global); }); +addActionHandler('loadMyCollectibleGifts', async (global, actions, payload): Promise => { + const { shouldRefresh } = payload || {}; + const currentUserId = global.currentUserId; + if (!currentUserId) return; + + const currentMyCollectibleGifts = global.myCollectibleGifts; + const localNextOffset = currentMyCollectibleGifts?.nextOffset; + + if (currentMyCollectibleGifts && !localNextOffset && !shouldRefresh) return; + + const peer = selectPeer(global, currentUserId); + if (!peer) return; + + const result = await callApi('fetchSavedStarGifts', { + peer, + offset: !shouldRefresh ? localNextOffset : undefined, + filter: { + sortType: 'byDate', + shouldIncludeUnique: true, + shouldIncludeUnlimited: false, + shouldIncludeUpgradable: false, + shouldIncludeLimited: false, + shouldIncludeDisplayed: true, + shouldIncludeHidden: true, + }, + }); + + if (!result) return; + + global = getGlobal(); + + const gifts = result.gifts; + + const byId = buildCollectionByCallback(gifts, (savedGift) => ( + [savedGift.gift.id, savedGift] + )); + + const ids = gifts.map((gift) => gift.gift.id); + + global = { + ...global, + myCollectibleGifts: { + byId: { + ...!shouldRefresh && (global.myCollectibleGifts?.byId || {}), + ...byId, + }, + ids: [ + ...!shouldRefresh ? (global.myCollectibleGifts?.ids || []) : [], + ...ids, + ], + nextOffset: result.nextOffset, + }, + }; + + setGlobal(global); +}); + addActionHandler('updateResaleGiftsFilter', (global, actions, payload): ActionReturnType => { const { filter, tabId = getCurrentTabId(), @@ -338,6 +371,9 @@ addActionHandler('reloadPeerSavedGifts', (global, actions, payload): ActionRetur actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id }); } }); + if (peerId === global.currentUserId) { + actions.loadMyCollectibleGifts({ shouldRefresh: true }); + } }); addActionHandler('loadStarsSubscriptions', async (global): Promise => { diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index 3324256cd..399c8f476 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -298,6 +298,8 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { global = updateTabState(global, { isWaitingForStarGiftTransfer: undefined, }, tabId); + + actions.reloadPeerSavedGifts({ peerId: global.currentUserId! }); } }); diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index 47bee44e6..f6d4375da 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -146,6 +146,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { }, tabId, }); + actions.reloadPeerSavedGifts({ peerId: starGiftModalState.forPeerId }); actions.requestConfetti({ withStars: true, tabId }); actions.closeGiftModal({ tabId }); } diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index be58a4883..40d956c71 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -434,6 +434,21 @@ addActionHandler('openGiftTransferModal', (global, actions, payload): ActionRetu addTabStateResetterAction('closeGiftTransferModal', 'giftTransferModal'); +addActionHandler('openGiftTransferConfirmModal', (global, actions, payload): ActionReturnType => { + const { + gift, recipientId, tabId = getCurrentTabId(), + } = payload; + + return updateTabState(global, { + giftTransferConfirmModal: { + gift, + recipientId, + }, + }, tabId); +}); + +addTabStateResetterAction('closeGiftTransferConfirmModal', 'giftTransferConfirmModal'); + addActionHandler('updateSelectedGiftCollection', (global, actions, payload): ActionReturnType => { const { peerId, collectionId, tabId = getCurrentTabId() } = payload; const tabState = selectTabState(global, tabId); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 8605ef06c..307a8847b 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -2544,6 +2544,9 @@ export interface ActionPayloads { loadPremiumGifts: undefined; loadTonGifts: undefined; loadStarGifts: undefined; + loadMyCollectibleGifts: { + shouldRefresh?: true; + } | undefined; updateResaleGiftsFilter: { filter: ResaleGiftsFilterOptions; } & WithTabId; @@ -2643,6 +2646,11 @@ export interface ActionPayloads { recipientId: string; } & WithTabId; closeGiftTransferModal: WithTabId | undefined; + openGiftTransferConfirmModal: { + gift: ApiSavedStarGift; + recipientId: string; + } & WithTabId; + closeGiftTransferConfirmModal: WithTabId | undefined; updateSelectedGiftCollection: { peerId: string; collectionId: number; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index ca0e1725e..de244de49 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -28,6 +28,7 @@ import type { ApiReaction, ApiReactionKey, ApiSavedReactionTag, + ApiSavedStarGift, ApiSession, ApiSponsoredMessage, ApiStarGiftCollection, @@ -311,6 +312,11 @@ export type GlobalState = { byId: Record; idsByCategory: Record; }; + myCollectibleGifts?: { + byId: Record; + ids: string[]; + nextOffset?: string; + }; starGiftCollections?: { byPeerId: Record; }; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index a89b0b912..a438bbdef 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -849,6 +849,11 @@ export type TabState = { gift: ApiSavedStarGift; }; + giftTransferConfirmModal?: { + gift: ApiSavedStarGift; + recipientId: string; + }; + giftUpgradeModal?: { sampleAttributes: ApiStarGiftAttribute[]; recipientId?: string; diff --git a/src/types/index.ts b/src/types/index.ts index 6613499e5..d574cb237 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -669,7 +669,7 @@ export type WebPageMediaSize = 'large' | 'small'; export type AttachmentCompression = 'compress' | 'original'; -export type StarGiftCategory = number | 'all' | 'limited' | 'stock' | 'resale'; +export type StarGiftCategory = 'all' | 'myCollectibles' | 'resale'; export type CallSound = ( 'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 9e2805f56..9341ad68e 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1232,6 +1232,7 @@ export interface LangPair { 'GiftPremiumDescriptionLink': undefined; 'StarsGiftHeader': undefined; 'StarsGiftHeaderSelf': undefined; + 'StarGiftDescriptionCollectibles': undefined; 'StarGiftDescriptionSelf': undefined; 'GiftLimited': undefined; 'GiftSoldOut': undefined; @@ -1304,8 +1305,6 @@ export interface LangPair { 'GiftWithdrawTitle': undefined; 'GiftWithdrawSubmit': undefined; 'AllGiftsCategory': undefined; - 'LimitedGiftsCategory': undefined; - 'StockGiftsCategory': undefined; 'PremiumGiftDescription': undefined; 'StarsReactionLinkText': undefined; 'StarsReactionLink': undefined; @@ -1578,7 +1577,8 @@ export interface LangPair { 'StarGiftPurchaseTransaction': undefined; 'ContextMenuItemMention': undefined; 'GiftRibbonResale': undefined; - 'GiftCategoryResale': undefined; + 'GiftCategoryCollectibles': undefined; + 'GiftCategoryMyGifts': undefined; 'GiftSortByPrice': undefined; 'GiftSortByNumber': undefined; 'ContextMenuItemSelectAll': undefined; @@ -1709,6 +1709,7 @@ export interface LangPair { 'GiftValueForSaleOnFragment': undefined; 'GiftValueForSaleOnTelegram': undefined; 'EmbeddedMessageNoCaption': undefined; + 'ConfirmBuyGiftForTonDescription': undefined; 'TitleGiftLocked': undefined; 'QuickPreview': undefined; }