From 92cdf9feb53fea17eb3395b7385ef860e600e3b3 Mon Sep 17 00:00:00 2001
From: zubiden <19638254+zubiden@users.noreply.github.com>
Date: Wed, 14 May 2025 19:02:27 +0300
Subject: [PATCH] Gifts: Support selling in profile
---
src/api/gramjs/apiBuilders/appConfig.ts | 6 +
src/api/gramjs/apiBuilders/calls.ts | 3 +-
src/api/gramjs/apiBuilders/gifts.ts | 11 +-
src/api/gramjs/apiBuilders/messageActions.ts | 11 +
src/api/gramjs/apiBuilders/payments.ts | 2 +
src/api/gramjs/gramjsBuilders/index.ts | 10 +
src/api/gramjs/methods/chats.ts | 20 +-
src/api/gramjs/methods/stars.ts | 15 +
src/api/gramjs/updates/mtpUpdateHandler.ts | 20 +-
src/api/types/messageActions.ts | 1 +
src/api/types/misc.ts | 3 +
src/api/types/payments.ts | 17 +-
src/api/types/stars.ts | 6 +
.../font-icons/crown-take-off-outline.svg | 1 +
src/assets/font-icons/crown-wear-outline.svg | 1 +
src/assets/font-icons/eye-crossed-outline.svg | 2 +-
src/assets/font-icons/eye-outline.svg | 2 +-
src/assets/font-icons/link-badge.svg | 2 +-
src/assets/font-icons/sell-outline.svg | 1 +
src/assets/font-icons/sell.svg | 1 +
src/assets/font-icons/unlist-outline.svg | 1 +
src/assets/font-icons/unlist.svg | 1 +
src/assets/localization/fallback.strings | 21 +
src/bundles/stars.ts | 1 +
src/components/common/gift/GiftMenuItems.tsx | 62 ++-
src/components/common/gift/GiftRibbon.tsx | 1 +
.../gift/GiftTransferPreview.module.scss} | 2 +-
.../common/gift/GiftTransferPreview.tsx | 60 +++
src/components/common/gift/SavedGift.tsx | 23 +-
.../middle/message/ActionMessageText.tsx | 15 +-
src/components/modals/ModalContainer.tsx | 3 +
.../modals/common/TableInfoModal.tsx | 6 +
.../modals/gift/UniqueGiftHeader.module.scss | 11 +
.../modals/gift/UniqueGiftHeader.tsx | 17 +
.../gift/info/GiftInfoModal.module.scss | 57 +++
.../modals/gift/info/GiftInfoModal.tsx | 153 ++++++-
.../GiftResalePriceComposerModal.async.tsx | 18 +
.../GiftResalePriceComposerModal.module.scss | 19 +
.../resale/GiftResalePriceComposerModal.tsx | 158 +++++++
.../gift/transfer/GiftTransferModal.tsx | 34 +-
.../modals/stars/helpers/transaction.ts | 7 +
.../transaction/StarsTransactionItem.tsx | 6 +-
.../transaction/StarsTransactionModal.tsx | 16 +-
src/components/ui/Modal.scss | 10 +
src/components/ui/Modal.tsx | 9 +
.../ui/ModalStarBalanceBar.module.scss | 46 ++
src/components/ui/ModalStarBalanceBar.tsx | 88 ++++
src/global/actions/api/payments.ts | 19 +-
src/global/actions/api/stars.ts | 37 +-
src/global/actions/apiUpdaters/payments.ts | 19 +
src/global/actions/ui/stars.ts | 15 +
src/global/helpers/payments.ts | 16 +
src/global/types/actions.ts | 17 +
src/global/types/tabState.ts | 5 +
src/lib/gramjs/tl/AllTLObjects.ts | 2 +-
src/lib/gramjs/tl/api.d.ts | 337 ++++++++++++--
src/lib/gramjs/tl/apiTl.ts | 48 +-
src/lib/gramjs/tl/static/api.json | 1 +
src/lib/gramjs/tl/static/api.tl | 64 ++-
src/styles/icons.scss | 416 +++++++++---------
src/styles/icons.woff | Bin 32860 -> 33560 bytes
src/styles/icons.woff2 | Bin 27460 -> 28064 bytes
src/types/icons/font.ts | 6 +
src/types/language.d.ts | 50 +++
src/util/localization/format.tsx | 2 +-
65 files changed, 1669 insertions(+), 365 deletions(-)
create mode 100644 src/assets/font-icons/crown-take-off-outline.svg
create mode 100644 src/assets/font-icons/crown-wear-outline.svg
create mode 100644 src/assets/font-icons/sell-outline.svg
create mode 100644 src/assets/font-icons/sell.svg
create mode 100644 src/assets/font-icons/unlist-outline.svg
create mode 100644 src/assets/font-icons/unlist.svg
rename src/components/{modals/gift/transfer/GiftTransferModal.module.scss => common/gift/GiftTransferPreview.module.scss} (97%)
create mode 100644 src/components/common/gift/GiftTransferPreview.tsx
create mode 100644 src/components/modals/gift/resale/GiftResalePriceComposerModal.async.tsx
create mode 100644 src/components/modals/gift/resale/GiftResalePriceComposerModal.module.scss
create mode 100644 src/components/modals/gift/resale/GiftResalePriceComposerModal.tsx
create mode 100644 src/components/ui/ModalStarBalanceBar.module.scss
create mode 100644 src/components/ui/ModalStarBalanceBar.tsx
diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts
index c01a7cd04..12321a380 100644
--- a/src/api/gramjs/apiBuilders/appConfig.ts
+++ b/src/api/gramjs/apiBuilders/appConfig.ts
@@ -95,6 +95,9 @@ export interface GramJsAppConfig extends LimitsConfig {
freeze_since_date?: number;
freeze_until_date?: number;
freeze_appeal_url?: string;
+ stars_stargift_resale_amount_max?: number;
+ stars_stargift_resale_amount_min?: number;
+ stars_stargift_resale_commission_permille?: number;
}
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@@ -191,5 +194,8 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
freezeSinceDate: appConfig.freeze_since_date,
freezeUntilDate: appConfig.freeze_until_date,
freezeAppealUrl: appConfig.freeze_appeal_url,
+ starsStargiftResaleAmountMin: appConfig.stars_stargift_resale_amount_min,
+ starsStargiftResaleAmountMax: appConfig.stars_stargift_resale_amount_max,
+ starsStargiftResaleCommissionPermille: appConfig.stars_stargift_resale_commission_permille,
};
}
diff --git a/src/api/gramjs/apiBuilders/calls.ts b/src/api/gramjs/apiBuilders/calls.ts
index 3ac157919..624af1cfe 100644
--- a/src/api/gramjs/apiBuilders/calls.ts
+++ b/src/api/gramjs/apiBuilders/calls.ts
@@ -102,7 +102,8 @@ export function buildApiGroupCall(groupCall: GramJs.TypeGroupCall): ApiGroupCall
}
export function getGroupCallId(groupCall: GramJs.TypeInputGroupCall) {
- return groupCall.id.toString();
+ if (groupCall instanceof GramJs.InputGroupCall) return groupCall.id.toString();
+ return undefined;
}
export function buildPhoneCall(call: GramJs.TypePhoneCall): ApiPhoneCall {
diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts
index dd33c8083..5d5d6c30b 100644
--- a/src/api/gramjs/apiBuilders/gifts.ts
+++ b/src/api/gramjs/apiBuilders/gifts.ts
@@ -18,7 +18,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
if (starGift instanceof GramJs.StarGiftUnique) {
const {
id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, slug, ownerAddress,
- giftAddress,
+ giftAddress, resellStars,
} = starGift;
return {
@@ -34,12 +34,13 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
issuedCount: availabilityIssued,
slug,
giftAddress,
+ resellPriceInStars: resellStars?.toJSNumber(),
};
}
const {
id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut,
- birthday, upgradeStars,
+ birthday, upgradeStars, resellMinStars, title,
} = starGift;
addDocumentToLocalDb(starGift.sticker);
@@ -60,6 +61,8 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
isSoldOut: soldOut,
isBirthday: birthday,
upgradeStars: upgradeStars?.toJSNumber(),
+ title,
+ resellMinStars: resellMinStars?.toJSNumber(),
};
}
@@ -132,7 +135,7 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId: string): ApiSavedStarGift {
const {
gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, upgradeStars, transferStars, canUpgrade,
- savedId, canExportAt, pinnedToTop,
+ savedId, canExportAt, pinnedToTop, canResellAt, canTransferAt,
} = userStarGift;
const inputGift: ApiInputSavedStarGift | undefined = savedId && peerId
@@ -154,6 +157,8 @@ export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId
inputGift,
savedId: savedId?.toString(),
canExportAt,
+ canResellAt,
+ canTransferAt,
isPinned: pinnedToTop,
};
}
diff --git a/src/api/gramjs/apiBuilders/messageActions.ts b/src/api/gramjs/apiBuilders/messageActions.ts
index 08b5df9fa..d77ca5774 100644
--- a/src/api/gramjs/apiBuilders/messageActions.ts
+++ b/src/api/gramjs/apiBuilders/messageActions.ts
@@ -187,6 +187,9 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
}
if (action instanceof GramJs.MessageActionGroupCall) {
const { call, duration } = action;
+ if (!(call instanceof GramJs.InputGroupCall)) {
+ return UNSUPPORTED_ACTION;
+ }
return {
mediaType: 'action',
type: 'groupCall',
@@ -199,6 +202,9 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
}
if (action instanceof GramJs.MessageActionInviteToGroupCall) {
const { call, users } = action;
+ if (!(call instanceof GramJs.InputGroupCall)) {
+ return UNSUPPORTED_ACTION;
+ }
return {
mediaType: 'action',
type: 'inviteToGroupCall',
@@ -211,6 +217,9 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
}
if (action instanceof GramJs.MessageActionGroupCallScheduled) {
const { call, scheduleDate } = action;
+ if (!(call instanceof GramJs.InputGroupCall)) {
+ return UNSUPPORTED_ACTION;
+ }
return {
mediaType: 'action',
type: 'groupCallScheduled',
@@ -393,6 +402,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
if (action instanceof GramJs.MessageActionStarGiftUnique) {
const {
upgrade, transferred, saved, refunded, gift, canExportAt, transferStars, fromId, peer, savedId,
+ resaleStars,
} = action;
const starGift = buildApiStarGift(gift);
@@ -411,6 +421,7 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
fromId: fromId && getApiChatIdFromMtpPeer(fromId),
peerId: peer && getApiChatIdFromMtpPeer(peer),
savedId: savedId && buildApiPeerId(savedId, 'user'),
+ resaleStars: resaleStars?.toJSNumber(),
};
}
if (action instanceof GramJs.MessageActionPaidMessagesPrice) {
diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts
index 3ac4b68ed..f11319852 100644
--- a/src/api/gramjs/apiBuilders/payments.ts
+++ b/src/api/gramjs/apiBuilders/payments.ts
@@ -536,6 +536,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
const {
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages,
+ stargiftResale,
} = transaction;
if (photo) {
@@ -567,6 +568,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
giveawayPostId,
starRefCommision,
isGiftUpgrade: stargiftUpgrade,
+ isGiftResale: stargiftResale,
paidMessages,
};
}
diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts
index d6aba1ff1..5719a12e0 100644
--- a/src/api/gramjs/gramjsBuilders/index.ts
+++ b/src/api/gramjs/gramjsBuilders/index.ts
@@ -687,6 +687,16 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
});
}
+ case 'stargiftResale': {
+ const {
+ peer, slug,
+ } = invoice;
+ return new GramJs.InputInvoiceStarGiftResale({
+ toId: buildInputPeer(peer.id, peer.accessHash),
+ slug,
+ });
+ }
+
case 'stargift': {
const {
peer, shouldHideName, giftId, message, shouldUpgrade,
diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts
index 91b88cdb6..f21cbe1a8 100644
--- a/src/api/gramjs/methods/chats.ts
+++ b/src/api/gramjs/methods/chats.ts
@@ -543,6 +543,8 @@ async function getFullChatInfo(chatId: string): Promise buildApiChatFromPreview(chat)).filter(Boolean);
+ const groupCall = call instanceof GramJs.InputGroupCall ? call : undefined;
+
return {
fullInfo: {
...(chatPhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(chatPhoto) }),
@@ -552,7 +554,7 @@ async function getFullChatInfo(chatId: string): Promise
\ No newline at end of file
diff --git a/src/assets/font-icons/crown-wear-outline.svg b/src/assets/font-icons/crown-wear-outline.svg
new file mode 100644
index 000000000..659240af3
--- /dev/null
+++ b/src/assets/font-icons/crown-wear-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/font-icons/eye-crossed-outline.svg b/src/assets/font-icons/eye-crossed-outline.svg
index 240e12ee6..184742538 100644
--- a/src/assets/font-icons/eye-crossed-outline.svg
+++ b/src/assets/font-icons/eye-crossed-outline.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/assets/font-icons/eye-outline.svg b/src/assets/font-icons/eye-outline.svg
index 0ebb379d5..e3619399b 100644
--- a/src/assets/font-icons/eye-outline.svg
+++ b/src/assets/font-icons/eye-outline.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/assets/font-icons/link-badge.svg b/src/assets/font-icons/link-badge.svg
index 934209e2e..cad871874 100644
--- a/src/assets/font-icons/link-badge.svg
+++ b/src/assets/font-icons/link-badge.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/assets/font-icons/sell-outline.svg b/src/assets/font-icons/sell-outline.svg
new file mode 100644
index 000000000..b4303b4f7
--- /dev/null
+++ b/src/assets/font-icons/sell-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/font-icons/sell.svg b/src/assets/font-icons/sell.svg
new file mode 100644
index 000000000..f5a8278e0
--- /dev/null
+++ b/src/assets/font-icons/sell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/font-icons/unlist-outline.svg b/src/assets/font-icons/unlist-outline.svg
new file mode 100644
index 000000000..8fc7e2c1e
--- /dev/null
+++ b/src/assets/font-icons/unlist-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/font-icons/unlist.svg b/src/assets/font-icons/unlist.svg
new file mode 100644
index 000000000..df95fccf1
--- /dev/null
+++ b/src/assets/font-icons/unlist.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings
index 24e37abdd..ec905802c 100644
--- a/src/assets/localization/fallback.strings
+++ b/src/assets/localization/fallback.strings
@@ -1485,6 +1485,7 @@
"GiftInfoWear" = "Wear";
"GiftInfoTakeOff" = "Take Off";
"GiftInfoTransfer" = "Transfer";
+"GiftInfoUnlist" = "Unlist";
"GiftTransferTitle" = "Transfer";
"GiftTransferTON" = "Send via Blockchain";
"GiftTransferTONBlocked" = "unlocks in {time}";
@@ -1953,5 +1954,25 @@
"ApiMessageActionPaidMessagesRefundedIncoming" = "{user} refunded **{stars}** to you";
"NotificationTitleNotSupportedInFrozenAccount" = "Your account is frozen";
"NotificationMessageNotSupportedInFrozenAccount" = "This action is not available";
+"NotificationGiftIsSale" = "{gift} is now for sale!";
+"NotificationGiftIsUnlist" = "{gift} is removed from sale.";
+"GiftRibbonSale" = "sale";
+"ButtonBuyGift" = "Buy for {stars}";
+"GiftInfoBuyGift" = "{user} is selling this gift and you can buy it.";
+"StarsGiftBought"= "You bought gift!";
+"ButtonSellGift" = "Sell for {stars}";
+"GiftSellTitle" = "Sell Gift";
+"Sell" = "Sell";
+"InputPlaceholderGiftResalePrice" = "Enter Price";
+"DescriptionComposerGiftResalePrice" = "You will receive **{stars}**.";
+"DescriptionComposerGiftMinimumPrice" = "Minimum price is **{stars}**.";
+"ApiMessageMessageActionResaleStarGiftUniqueOutgoing" = "You paid {stars} for {gift}";
+"ApiMessageMessageActionResaleStarGiftUniqueIncoming" = "You received {stars} from selling {gift}";
+"ModalStarsBalanceBarDescription" = "Your balance is **{stars}**";
+"NotificationGiftCanResellAt" = "You will be able to resell this gift on {date}.";
+"NotificationGiftCanTransferAt" = "You can transfer this gift after {date}.";
+"StarGiftSaleTransaction" = "Gift Purchase";
+"StarGiftPurchaseTransaction" = "Gift Sale";
+"GiftBuyConfirmDescription" = "Do you want to buy **{gift}** for **{stars}**?";
"ComposerTitleForwardFrom" = "From: **{users}**";
"ContextMenuItemMention" = "Mention";
diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts
index 6adb938fb..e5fc157df 100644
--- a/src/bundles/stars.ts
+++ b/src/bundles/stars.ts
@@ -8,6 +8,7 @@ export { default as PaidReactionModal } from '../components/modals/paidReaction/
export { default as GiftModal } from '../components/modals/gift/GiftModal';
export { default as GiftRecipientPicker } from '../components/modals/gift/recipient/GiftRecipientPicker';
export { default as GiftInfoModal } from '../components/modals/gift/info/GiftInfoModal';
+export { default as GiftResalePriceComposerModal } from '../components/modals/gift/resale/GiftResalePriceComposerModal';
export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal';
export { default as GiftStatusInfoModal } from '../components/modals/gift/status/GiftStatusInfoModal';
export { default as GiftWithdrawModal } from '../components/modals/gift/withdraw/GiftWithdrawModal';
diff --git a/src/components/common/gift/GiftMenuItems.tsx b/src/components/common/gift/GiftMenuItems.tsx
index b47b5730d..2f05a0e1f 100644
--- a/src/components/common/gift/GiftMenuItems.tsx
+++ b/src/components/common/gift/GiftMenuItems.tsx
@@ -7,9 +7,12 @@ import type {
import { DEFAULT_STATUS_ICON_ID, TME_LINK_PREFIX } from '../../../config';
import { copyTextToClipboard } from '../../../util/clipboard';
+import { formatDateAtTime } from '../../../util/dates/dateFormat';
+import { getServerTime } from '../../../util/serverTime';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
+import useOldLang from '../../../hooks/useOldLang';
import MenuItem from '../../ui/MenuItem';
@@ -32,13 +35,17 @@ const GiftMenuItems = ({
showNotification,
openChatWithDraft,
openGiftTransferModal,
+ openGiftResalePriceComposerModal,
openGiftStatusInfoModal,
setEmojiStatus,
toggleSavedGiftPinned,
changeGiftVisibility,
+ updateStarGiftPrice,
+ closeGiftInfoModal,
} = getActions();
const lang = useLang();
+ const oldLang = useOldLang();
const isSavedGift = typeGift && 'gift' in typeGift;
const savedGift = isSavedGift ? typeGift : undefined;
@@ -62,6 +69,7 @@ const GiftMenuItems = ({
const isGiftUnique = gift && gift.type === 'starGiftUnique';
const canTakeOff = isGiftUnique && currenUniqueEmojiStatusSlug === gift.slug;
const canWear = userCollectibleStatus && !canTakeOff;
+ const giftResalePrice = isGiftUnique ? gift.resellPriceInStars : undefined;
const hasPinOptions = canManage && savedGift && !savedGift.isUnsaved && isGiftUnique;
@@ -84,10 +92,48 @@ const GiftMenuItems = ({
});
const handleTransfer = useLastCallback(() => {
- if (savedGift?.gift.type !== 'starGiftUnique') return;
+ if (!savedGift || savedGift?.gift.type !== 'starGiftUnique') return;
+
+ if (savedGift.canTransferAt && savedGift.canTransferAt > getServerTime()) {
+ showNotification({
+ message: {
+ key: 'NotificationGiftCanTransferAt',
+ variables: { date: formatDateAtTime(oldLang, savedGift.canTransferAt * 1000) },
+ },
+ });
+ return;
+ }
+
openGiftTransferModal({ gift: savedGift });
});
+ const handleSell = useLastCallback(() => {
+ if (!savedGift) return;
+ if (savedGift.canResellAt && savedGift.canResellAt > getServerTime()) {
+ showNotification({
+ message: {
+ key: 'NotificationGiftCanResellAt',
+ variables: { date: formatDateAtTime(oldLang, savedGift.canResellAt * 1000) },
+ },
+ });
+ return;
+ }
+ openGiftResalePriceComposerModal({ peerId, gift: savedGift });
+ });
+
+ const handleUnsell = useLastCallback(() => {
+ if (!savedGift || savedGift.gift.type !== 'starGiftUnique' || !savedGift.inputGift) return;
+ closeGiftInfoModal();
+ updateStarGiftPrice({ gift: savedGift.inputGift, price: 0 });
+ showNotification({
+ icon: 'unlist-outline',
+ message: {
+ key: 'NotificationGiftIsUnlist',
+ variables: { gift: lang('GiftUnique', { title: savedGift.gift.title, number: savedGift.gift.number }) },
+ },
+ });
+ });
+
const handleWear = useLastCallback(() => {
if (gift?.type !== 'starGiftUnique' || !userCollectibleStatus) return;
openGiftStatusInfoModal({ emojiStatus: userCollectibleStatus });
@@ -123,18 +169,28 @@ const GiftMenuItems = ({
{lang('GiftInfoTransfer')}
)}
+ {canManage && isGiftUnique && !giftResalePrice && (
+
+ )}
+ {canManage && isGiftUnique && giftResalePrice && (
+
+ )}
{canManage && savedGift && (
)}
{canWear && (
-
)}
+ {resellPrice && (
+
+
+ {formatStarsTransactionAmount(lang, resellPrice)}
+
+
+
+ )}
);
};
diff --git a/src/components/modals/gift/info/GiftInfoModal.module.scss b/src/components/modals/gift/info/GiftInfoModal.module.scss
index dc3dc4475..b6726ab76 100644
--- a/src/components/modals/gift/info/GiftInfoModal.module.scss
+++ b/src/components/modals/gift/info/GiftInfoModal.module.scss
@@ -18,6 +18,63 @@
color: var(--color-error);
}
+.headerSplitButton {
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ right: 0.375rem;
+}
+
+.headerButton,
+.giftResalePriceContainer {
+ height: 1.75rem;
+ width: fit-content;
+ font-size: 1rem;
+ font-weight: var(--font-weight-medium);
+
+ outline: none !important;
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ color: white;
+ border-radius: 1rem;
+ background-color: rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(25px);
+ pointer-events: auto;
+ padding: 0.25rem;
+ padding-inline: 0.625rem;
+}
+
+.giftResalePriceContainer {
+ font-size: 0.75rem;
+}
+
+.giftResalePriceStar {
+ margin-inline-start: 0 !important;
+}
+
+.headerButton {
+ position: relative;
+ cursor: var(--custom-cursor, pointer);
+ flex-shrink: 0;
+ overflow: hidden;
+ transition: background-color 0.15s;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+}
+
+.left {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.right {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+}
+
.description {
text-align: center;
color: var(--_color-description, var(--color-text));
diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx
index 783cc8d87..7156ffa9a 100644
--- a/src/components/modals/gift/info/GiftInfoModal.tsx
+++ b/src/components/modals/gift/info/GiftInfoModal.tsx
@@ -1,10 +1,11 @@
import type { FC, TeactNode } from '../../../../lib/teact/teact';
-import React, { memo, useMemo } from '../../../../lib/teact/teact';
+import React, { memo, useMemo, useState } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global';
import type {
ApiEmojiStatusType,
ApiPeer,
+ ApiUser,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
@@ -31,6 +32,7 @@ import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import BadgeButton from '../../../common/BadgeButton';
import GiftMenuItems from '../../../common/gift/GiftMenuItems';
+import GiftTransferPreview from '../../../common/gift/GiftTransferPreview';
import Icon from '../../../common/icons/Icon';
import SafeLink from '../../../common/SafeLink';
import Button from '../../../ui/Button';
@@ -55,6 +57,7 @@ type StateProps = {
currentUserEmojiStatus?: ApiEmojiStatusType;
collectibleEmojiStatuses?: ApiEmojiStatusType[];
tonExplorerUrl?: string;
+ currentUser?: ApiUser;
};
const STICKER_SIZE = 120;
@@ -69,6 +72,7 @@ const GiftInfoModal = ({
currentUserEmojiStatus,
collectibleEmojiStatuses,
tonExplorerUrl,
+ currentUser,
}: OwnProps & StateProps) => {
const {
closeGiftInfoModal,
@@ -78,12 +82,14 @@ const GiftInfoModal = ({
focusMessage,
openGiftUpgradeModal,
showNotification,
+ buyStarGift,
} = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
const lang = useLang();
const oldLang = useOldLang();
+ const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
@@ -106,12 +112,24 @@ const GiftInfoModal = ({
const hasConvertOption = canConvertDifference > 0 && Boolean(savedGift?.starsToConvert);
const isGiftUnique = gift && gift.type === 'starGiftUnique';
+ const uniqueGift = isGiftUnique ? gift : undefined;
const canFocusUpgrade = Boolean(savedGift?.upgradeMsgId);
const canManage = !canFocusUpgrade && savedGift?.inputGift && (
isTargetChat ? hasAdminRights : renderingTargetPeer?.id === currentUserId
);
+ const resellPriceInStars = isGiftUnique ? gift.resellPriceInStars : undefined;
+ const canBuyGift = !canManage && Boolean(resellPriceInStars);
+
+ const giftOwnerTitle = (() => {
+ if (!isGiftUnique) return undefined;
+ const { ownerName, ownerId } = gift;
+ const global = getGlobal(); // Peer titles do not need to be reactive
+ const owner = ownerId ? selectPeer(global, ownerId) : undefined;
+ return owner ? getPeerTitle(lang, owner) : ownerName;
+ })();
+
const handleClose = useLastCallback(() => {
closeGiftInfoModal();
});
@@ -142,26 +160,57 @@ const GiftInfoModal = ({
openGiftUpgradeModal({ giftId: savedGift.gift.id, gift: savedGift });
});
+ const handleBuyGift = useLastCallback(() => {
+ if (!savedGift || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
+ setIsConfirmModalOpen(true);
+ });
+
+ const closeConfirmModal = useLastCallback(() => {
+ setIsConfirmModalOpen(false);
+ });
+
+ const handleConfirmBuyGift = useLastCallback(() => {
+ if (!savedGift || gift?.type !== 'starGiftUnique' || !gift.resellPriceInStars) return;
+ closeConfirmModal();
+ buyStarGift({ slug: gift.slug, stars: gift.resellPriceInStars });
+ });
+
const giftAttributes = useMemo(() => {
return gift && getGiftAttributes(gift);
}, [gift]);
const SettingsMenuButton: FC<{ onTrigger: () => void; isMenuOpen?: boolean }> = useMemo(() => {
- return ({ onTrigger, isMenuOpen }) => (
-