diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts index 8eca61b40..63cb07bcf 100644 --- a/src/api/gramjs/apiBuilders/gifts.ts +++ b/src/api/gramjs/apiBuilders/gifts.ts @@ -36,7 +36,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift { const { id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, slug, ownerAddress, giftAddress, resellAmount, releasedBy, resaleTonOnly, requirePremium, valueCurrency, valueAmount, giftId, - valueUsdAmount, + valueUsdAmount, burned, crafted, } = starGift; return { @@ -61,6 +61,8 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift { valueUsdAmount: toJSNumber(valueUsdAmount), regularGiftId: giftId.toString(), offerMinStars: starGift.offerMinStars, + isBurned: burned, + isCrafted: crafted, }; } diff --git a/src/api/gramjs/methods/stars.ts b/src/api/gramjs/methods/stars.ts index 236d9b12e..d1e91d475 100644 --- a/src/api/gramjs/methods/stars.ts +++ b/src/api/gramjs/methods/stars.ts @@ -1,4 +1,5 @@ import { Api as GramJs } from '../../../lib/gramjs'; +import { RPCError } from '../../../lib/gramjs/errors'; import type { GiftProfileFilterOptions, ResaleGiftsFilterOptions } from '../../../types'; import type { @@ -389,13 +390,22 @@ export async function fetchStarsTopupOptions() { export async function fetchUniqueStarGift({ slug }: { slug: string; }) { - const result = await invokeRequest(new GramJs.payments.GetUniqueStarGift({ slug })); + try { + const result = await invokeRequest(new GramJs.payments.GetUniqueStarGift({ slug }), { + shouldThrow: true, + }); - if (!result) return undefined; + if (!result) return undefined; - const gift = buildApiStarGift(result.gift); - if (gift.type !== 'starGiftUnique') return undefined; - return gift; + const gift = buildApiStarGift(result.gift); + if (gift.type !== 'starGiftUnique') return undefined; + return gift; + } catch (err) { + if (err instanceof RPCError) { + return wrapError(err); + } + return undefined; + } } export async function fetchStarGiftUpgradePreview({ diff --git a/src/api/types/stars.ts b/src/api/types/stars.ts index f3aab7754..7da17396e 100644 --- a/src/api/types/stars.ts +++ b/src/api/types/stars.ts @@ -61,6 +61,8 @@ export interface ApiStarGiftUnique { valueAmount?: number; valueUsdAmount?: number; offerMinStars?: number; + isBurned?: true; + isCrafted?: true; } export type ApiStarGift = ApiStarGiftRegular | ApiStarGiftUnique; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 815fac3c8..4c2c720d2 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1572,14 +1572,15 @@ "GiftInfoOwner" = "Owner"; "GiftInfoIssued" = "{issued}/{total} issued"; "GiftInfoCollectible" = "Collectible #{number}"; +"GiftInfoUniqueTitle" = "{name} {number}"; "GiftSavedNumber" = "#{number}"; "GiftAttributeModel" = "Model"; "GiftAttributeBackdrop" = "Backdrop"; "GiftAttributeSymbol" = "Symbol"; -"GiftRarityUncommon" = "Uncommon"; -"GiftRarityRare" = "Rare"; -"GiftRarityEpic" = "Epic"; -"GiftRarityLegendary" = "Legendary"; +"GiftRarityUncommon" = "uncommon"; +"GiftRarityRare" = "rare"; +"GiftRarityEpic" = "epic"; +"GiftRarityLegendary" = "legendary"; "GiftInfoPeerOriginalInfo" = "Gifted to {peer} on {date}."; "GiftInfoPeerOriginalInfoSender" = "Gifted by {sender} to {peer} on {date}."; "GiftInfoPeerOriginalInfoText" = "Gifted to {peer} on {date} with comment \"{text}\"."; @@ -1923,6 +1924,8 @@ "ActionStarGiftTransferredChannelYou" = "You transferred a gift to {channel}"; "ActionStarGiftTransferredMine" = "You transferred a gift to {user}"; "ActionStarGiftTransferredSelf" = "You transferred a unique collectible"; +"ActionStarGiftCraftedSelf" = "You crafted a new gift!"; +"ActionStarGiftCrafted" = "Crafted Gift"; "ActionStarGiftTransferredUnknown" = "Someone transferred you a gift"; "ActionStarGiftTransferredUnknownChannel" = "Someone transferred a gift to {channel}"; "ActionStarGiftSoldFromOffer" = "You sold {gift} to {user} for {cost}"; @@ -1950,6 +1953,8 @@ "ActionStarGiftUnpack" = "Unpack"; "ActionStarGiftLimitedRibbon" = "1 of {total}"; "ActionStarGiftUniqueRibbon" = "gift"; +"ActionStarGiftUniqueBurnedRibbon" = "burned"; +"ActionStarGiftUniqueBurnedError" = "Sorry, this gift has already been burned."; "ActionStarGiftUniqueModel" = "Model"; "ActionStarGiftUniqueBackdrop" = "Backdrop"; "ActionStarGiftUniqueSymbol" = "Symbol"; diff --git a/src/components/common/GiftRarityBadge.module.scss b/src/components/common/GiftRarityBadge.module.scss new file mode 100644 index 000000000..1db808fc0 --- /dev/null +++ b/src/components/common/GiftRarityBadge.module.scss @@ -0,0 +1,21 @@ +.root { + &.uncommon { + color: var(--color-gift-uncommon); + background-color: var(--color-gift-uncommon-bg); + } + + &.rare { + color: var(--color-gift-rare); + background-color: var(--color-gift-rare-bg); + } + + &.epic { + color: var(--color-gift-epic); + background-color: var(--color-gift-epic-bg); + } + + &.legendary { + color: var(--color-gift-legendary); + background-color: var(--color-gift-legendary-bg); + } +} diff --git a/src/components/common/GiftRarityBadge.tsx b/src/components/common/GiftRarityBadge.tsx new file mode 100644 index 000000000..04f2f80cd --- /dev/null +++ b/src/components/common/GiftRarityBadge.tsx @@ -0,0 +1,28 @@ +import { memo } from '../../lib/teact/teact'; + +import type { ApiStarGiftAttributeRarity } from '../../api/types'; + +import buildClassName from '../../util/buildClassName'; +import { getGiftRarityTitle } from './helpers/gifts'; + +import useLang from '../../hooks/useLang'; + +import BadgeButton from './BadgeButton'; + +import styles from './GiftRarityBadge.module.scss'; + +type OwnProps = { + rarity: ApiStarGiftAttributeRarity; +}; + +const GiftRarityBadge = ({ rarity }: OwnProps) => { + const lang = useLang(); + + return ( + + {getGiftRarityTitle(lang, rarity)} + + ); +}; + +export default memo(GiftRarityBadge); diff --git a/src/components/middle/message/ActionMessage.tsx b/src/components/middle/message/ActionMessage.tsx index 0a7a396b6..df9c7a5b7 100644 --- a/src/components/middle/message/ActionMessage.tsx +++ b/src/components/middle/message/ActionMessage.tsx @@ -158,6 +158,7 @@ const ActionMessage = ({ focusMessage, openGiftOfferAcceptModal, declineStarGiftOffer, + showNotification, } = getActions(); const ref = useRef(); @@ -371,8 +372,19 @@ const ActionMessage = ({ break; } - case 'starGift': + case 'starGift': { + openGiftInfoModalFromMessage({ + chatId: message.chatId, + messageId: message.id, + }); + break; + } + case 'starGiftUnique': { + if (action.gift.isBurned) { + showNotification({ message: lang('ActionStarGiftUniqueBurnedError') }); + break; + } openGiftInfoModalFromMessage({ chatId: message.chatId, messageId: message.id, diff --git a/src/components/middle/message/ActionMessageText.tsx b/src/components/middle/message/ActionMessageText.tsx index eb5e30dc4..5a410d726 100644 --- a/src/components/middle/message/ActionMessageText.tsx +++ b/src/components/middle/message/ActionMessageText.tsx @@ -702,6 +702,7 @@ const ActionMessageText = ({ if (isSavedMessages) { if (isUpgrade) return lang('ActionStarGiftUpgradedSelf'); if (isTransferred) return lang('ActionStarGiftTransferredSelf'); + if (gift.isCrafted) return lang('ActionStarGiftCraftedSelf'); } if (isUpgrade) { diff --git a/src/components/middle/message/actions/StarGiftUnique.tsx b/src/components/middle/message/actions/StarGiftUnique.tsx index 4c2ddbf18..75601cead 100644 --- a/src/components/middle/message/actions/StarGiftUnique.tsx +++ b/src/components/middle/message/actions/StarGiftUnique.tsx @@ -121,20 +121,24 @@ const StarGiftAction = ({ )}

- {isSelf ? lang('ActionStarGiftSelf') : lang( - shouldShowFrom ? 'ActionStarGiftFrom' : 'ActionStarGiftTo', - { - peer: renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle), - }, - { - withNodes: true, - }, - )} + {isSelf + ? (action.gift.isCrafted ? lang('ActionStarGiftCrafted') : lang('ActionStarGiftSelf')) + : lang( + shouldShowFrom ? 'ActionStarGiftFrom' : 'ActionStarGiftTo', + { + peer: renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle), + }, + { + withNodes: true, + }, + )}

{lang('GiftUnique', { title: action.gift.title, number: action.gift.number })} diff --git a/src/components/modals/gift/UniqueGiftHeader.tsx b/src/components/modals/gift/UniqueGiftHeader.tsx index 924fb49c4..04b6d46d0 100644 --- a/src/components/modals/gift/UniqueGiftHeader.tsx +++ b/src/components/modals/gift/UniqueGiftHeader.tsx @@ -34,7 +34,7 @@ type OwnProps = { modelAttribute: ApiStarGiftAttributeModel; backdropAttribute: ApiStarGiftAttributeBackdrop; patternAttribute: ApiStarGiftAttributePattern; - title?: string; + title?: TeactNode; badge?: TeactNode; subtitle?: TeactNode; subtitlePeer?: ApiPeer; @@ -114,7 +114,7 @@ const UniqueGiftHeader = ({ {Boolean(badge) && (
{badge}
)} - {title &&

{title}

} + {Boolean(title) &&

{title}

} {Boolean(subtitle) && (
{ - if (!gift || gift.type !== 'starGiftUnique') return undefined; - - if (releasedByPeer) { - const releasedByUsername = `@${getMainUsername(releasedByPeer)}`; - const ownerTitle = releasedByUsername || getPeerTitle(lang, releasedByPeer); - const fallbackText = isApiPeerUser(releasedByPeer) - ? lang('ActionFallbackUser') - : lang('ActionFallbackChannel'); - - return lang('GiftInfoCollectibleBy', { - number: gift.number, owner: ownerTitle || fallbackText }, { - withNodes: true, - withMarkdown: true, - }); - } - - return lang('GiftInfoCollectible', { number: gift.number }); - }, [gift, releasedByPeer, lang]); - const starGiftUniqueSlug = gift?.type === 'starGiftUnique' ? gift.slug : undefined; const selfCollectibleStatus = useMemo(() => { @@ -322,6 +303,42 @@ const GiftInfoModal = ({ return gift && getGiftAttributes(gift); }, [gift]); + const uniqueGiftTitle = useMemo(() => { + if (!gift || gift.type !== 'starGiftUnique' || !giftAttributes?.backdrop) return undefined; + + const numberColor = giftAttributes.backdrop.textColor; + const digitCount = String(gift.number).length; + const numberSizeClass = digitCount >= 6 ? styles.small : styles.regular; + const styledNumber = ( + + {lang('GiftSavedNumber', { number: gift.number })} + + ); + + return lang('GiftInfoUniqueTitle', { + name: gift.title, + number: styledNumber, + }, { withNodes: true }); + }, [gift, giftAttributes, lang]); + + const uniqueGiftSubtitle = useMemo(() => { + if (!gift || gift.type !== 'starGiftUnique') return undefined; + + if (releasedByPeer) { + const releasedByUsername = `@${getMainUsername(releasedByPeer)}`; + const ownerTitle = releasedByUsername || getPeerTitle(lang, releasedByPeer); + const fallbackText = isApiPeerUser(releasedByPeer) + ? lang('ActionFallbackUser') + : lang('ActionFallbackChannel'); + + return ownerTitle || fallbackText; + } + + const modelName = giftAttributes?.model?.name; + + return modelName; + }, [gift, giftAttributes, releasedByPeer, lang]); + const renderFooterButton = useLastCallback(() => { if (canBuyGift) { return ( @@ -551,8 +568,8 @@ const GiftInfoModal = ({ backdropAttribute={giftAttributes!.backdrop!} patternAttribute={giftAttributes!.pattern!} modelAttribute={giftAttributes!.model!} - title={gift.title} - subtitle={giftSubtitle} + title={uniqueGiftTitle} + subtitle={uniqueGiftSubtitle} subtitlePeer={releasedByPeer} showManageButtons={canManage} savedGift={savedGift} @@ -699,7 +716,7 @@ const GiftInfoModal = ({ > {model.name} - {getGiftRarityTitle(lang, model.rarity)} + , ]); } @@ -856,8 +873,8 @@ const GiftInfoModal = ({ canManage, hasConvertOption, isSender, oldLang, tonExplorerUrl, gift, giftAttributes, renderFooterButton, isTargetChat, isGiftUnique, saleDateInfo, - canBuyGift, giftOwnerTitle, resellPrice, giftSubtitle, - releasedByPeer, handleSymbolClick, handleBackdropClick, handleModelClick, + canBuyGift, giftOwnerTitle, resellPrice, uniqueGiftTitle, uniqueGiftSubtitle, releasedByPeer, + handleSymbolClick, handleBackdropClick, handleModelClick, ]); const moreMenuItems = typeGift && ( diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index dca1b3984..0a7d6450b 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -1234,19 +1234,20 @@ addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promi slug, tabId = getCurrentTabId(), } = payload; - const gift = await callApi('fetchUniqueStarGift', { slug }); + const result = await callApi('fetchUniqueStarGift', { slug }); - if (!gift) { + if (!result || 'error' in result) { + const isBurned = result && 'error' in result && result.errorMessage === 'STARGIFT_ALREADY_BURNED'; actions.showNotification({ message: { - key: 'GiftWasNotFound', + key: isBurned ? 'ActionStarGiftUniqueBurnedError' : 'GiftWasNotFound', }, tabId, }); return; } - actions.openGiftInfoModal({ gift, tabId }); + actions.openGiftInfoModal({ gift: result, tabId }); }); addActionHandler('openGiftAuctionBySlug', async (global, actions, payload): Promise => { diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 1a8e8837d..4183dac30 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -209,6 +209,15 @@ $color-message-story-mention-to: #74bcff; --color-stars: #FFAA00; --color-heart: #ff3c32; + --color-gift-uncommon: #40A920; + --color-gift-uncommon-bg: rgba(64, 169, 32, 0.15); + --color-gift-rare: #11AABE; + --color-gift-rare-bg: rgba(17, 170, 190, 0.15); + --color-gift-epic: #955CDB; + --color-gift-epic-bg: rgba(149, 92, 219, 0.15); + --color-gift-legendary: #BF7600; + --color-gift-legendary-bg: rgba(191, 118, 0, 0.15); + --color-negative-progress: #CE4C47; --vh: 1vh; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 537cf28e4..fe287703e 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1521,6 +1521,8 @@ export interface LangPair { 'ActionGiftUniqueSent': undefined; 'ActionStarGiftUpgradedSelf': undefined; 'ActionStarGiftTransferredSelf': undefined; + 'ActionStarGiftCraftedSelf': undefined; + 'ActionStarGiftCrafted': undefined; 'ActionStarGiftTransferredUnknown': undefined; 'ActionStarGiftNoConvertTextYou': undefined; 'ActionStarGiftDisplaying': undefined; @@ -1529,6 +1531,8 @@ export interface LangPair { 'ActionStarGiftUpgraded': undefined; 'ActionStarGiftUnpack': undefined; 'ActionStarGiftUniqueRibbon': undefined; + 'ActionStarGiftUniqueBurnedRibbon': undefined; + 'ActionStarGiftUniqueBurnedError': undefined; 'ActionStarGiftUniqueModel': undefined; 'ActionStarGiftUniqueBackdrop': undefined; 'ActionStarGiftUniqueSymbol': undefined; @@ -2377,6 +2381,10 @@ export interface LangPairWithVariables { 'GiftInfoCollectible': { 'number': V; }; + 'GiftInfoUniqueTitle': { + 'name': V; + 'number': V; + }; 'GiftSavedNumber': { 'number': V; };