From aecd1d216f0b899b765eb9c47eb8ac268004c222 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Tue, 21 Jan 2025 18:21:59 +0100 Subject: [PATCH] Star Gifts: Support upgrade option (#5475) --- .eslintignore | 4 +- src/api/gramjs/apiBuilders/gifts.ts | 8 +- src/api/gramjs/apiBuilders/messages.ts | 11 +- src/api/gramjs/gramjsBuilders/index.ts | 10 +- src/api/gramjs/methods/payments.ts | 36 +- src/api/types/messages.ts | 25 +- src/api/types/payments.ts | 4 + src/assets/font-icons/auction.svg | 1 + src/assets/font-icons/diamond.svg | 1 + src/assets/font-icons/trade.svg | 1 + src/assets/localization/fallback.strings | 40 +- src/bundles/stars.ts | 1 + .../common/AboutMonetizationModal.tsx | 1 + .../common/AnimatedIconFromSticker.tsx | 2 +- .../AnimatedIconWithPreview.module.scss | 2 +- src/components/common/helpers/gifts.ts | 13 +- .../helpers/renderActionMessageText.tsx | 14 +- .../profile/RadialPatternBackground.tsx | 67 +-- .../left/main/hooks/useChatListEntry.tsx | 11 +- src/components/middle/ActionMessage.tsx | 20 +- src/components/middle/message/WebPage.tsx | 3 + .../middle/message/helpers/webpageType.ts | 4 +- src/components/modals/ModalContainer.tsx | 5 +- .../modals/aboutAds/AboutAdsModal.module.scss | 4 +- .../modals/aboutAds/AboutAdsModal.tsx | 1 + .../modals/common/TableAboutModal.module.scss | 1 + .../modals/common/TableAboutModal.tsx | 12 +- .../modals/gift/GiftComposer.module.scss | 25 +- src/components/modals/gift/GiftComposer.tsx | 73 ++- src/components/modals/gift/GiftModal.tsx | 13 +- .../modals/gift/StarGiftCategoryList.tsx | 14 +- .../modals/gift/UniqueGiftHeader.module.scss | 55 +++ .../modals/gift/UniqueGiftHeader.tsx | 80 +++ .../gift/info/GiftInfoModal.module.scss | 11 +- .../modals/gift/info/GiftInfoModal.tsx | 100 ++-- .../gift/upgrade/GiftUpgradeModal.async.tsx | 18 + .../gift/upgrade/GiftUpgradeModal.module.scss | 11 + .../modals/gift/upgrade/GiftUpgradeModal.tsx | 206 ++++++++ .../modals/stars/StarsPaymentModal.tsx | 2 +- src/components/ui/ListItem.scss | 2 +- src/components/ui/Transition.scss | 25 + src/components/ui/Transition.tsx | 4 +- src/global/actions/api/payments.ts | 149 ++++-- src/global/actions/api/stars.ts | 75 ++- src/global/actions/apiUpdaters/misc.ts | 32 +- src/global/actions/ui/stars.ts | 10 + src/global/helpers/payments.ts | 12 +- src/global/initialState.ts | 6 - src/global/types/actions.ts | 15 +- src/global/types/globalState.ts | 6 +- src/global/types/tabState.ts | 9 + .../animations/useTransitionActiveKey.ts | 17 + src/lib/gramjs/tl/api.d.ts | 7 +- src/lib/gramjs/tl/apiTl.ts | 4 + src/lib/gramjs/tl/static/api.json | 47 +- src/styles/icons.scss | 464 +++++++++--------- src/styles/icons.woff | Bin 31072 -> 31588 bytes src/styles/icons.woff2 | Bin 25972 -> 26400 bytes src/types/icons/font.ts | 3 + src/types/index.ts | 1 + src/types/language.d.ts | 64 ++- src/util/localization/format.tsx | 8 +- src/util/notifications.ts | 4 +- 63 files changed, 1344 insertions(+), 530 deletions(-) create mode 100644 src/assets/font-icons/auction.svg create mode 100644 src/assets/font-icons/diamond.svg create mode 100644 src/assets/font-icons/trade.svg create mode 100644 src/components/modals/gift/UniqueGiftHeader.module.scss create mode 100644 src/components/modals/gift/UniqueGiftHeader.tsx create mode 100644 src/components/modals/gift/upgrade/GiftUpgradeModal.async.tsx create mode 100644 src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss create mode 100644 src/components/modals/gift/upgrade/GiftUpgradeModal.tsx create mode 100644 src/hooks/animations/useTransitionActiveKey.ts diff --git a/.eslintignore b/.eslintignore index d82cb90fb..323e11c16 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,10 +2,10 @@ src/lib/rlottie/rlottie-wasm.js src/lib/video-preview/polyfill src/lib/fasttextweb/fasttext-wasm.js -src/lib/gramjs/tl/types-generator/template.js +src/lib/gramjs/tl/types-generator/template.ts src/lib/gramjs/tl/api.d.ts src/lib/gramjs/tl/apiTl.ts -src/lib/gramjs/tl/schemaTl.js +src/lib/gramjs/tl/schemaTl.ts src/lib/lovely-chart diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts index 5ea94e483..e0307d4e2 100644 --- a/src/api/gramjs/apiBuilders/gifts.ts +++ b/src/api/gramjs/apiBuilders/gifts.ts @@ -15,7 +15,7 @@ import { buildStickerFromDocument } from './symbols'; export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift { if (starGift instanceof GramJs.StarGiftUnique) { const { - id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, + id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, slug, } = starGift; return { @@ -28,6 +28,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift { title, totalCount: availabilityTotal, issuedCount: availabilityIssued, + slug, }; } @@ -125,7 +126,7 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut export function buildApiUserStarGift(userStarGift: GramJs.UserStarGift): ApiUserStarGift { const { - gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, + gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, upgradeStars, transferStars, canUpgrade, } = userStarGift; return { @@ -137,5 +138,8 @@ export function buildApiUserStarGift(userStarGift: GramJs.UserStarGift): ApiUser messageId: msgId, isNameHidden: nameHidden, isUnsaved: unsaved, + canUpgrade, + alreadyPaidUpgradeStars: upgradeStars?.toJSNumber(), + transferStars: transferStars?.toJSNumber(), }; } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 290a93508..10c0f8d97 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -26,6 +26,8 @@ import type { ApiReplyInfo, ApiReplyKeyboard, ApiSponsoredMessage, + ApiStarGiftRegular, + ApiStarGiftUnique, ApiSticker, ApiStory, ApiStorySkipped, @@ -369,7 +371,7 @@ export function buildApiFactCheck(factCheck: GramJs.FactCheck): ApiFactCheck { function buildApiMessageActionStarGift(action: GramJs.MessageActionStarGift) : ApiMessageActionStarGift { const { - nameHidden, saved, converted, gift, message, convertStars, canUpgrade, upgraded, upgradeMsgId, + nameHidden, saved, converted, gift, message, convertStars, canUpgrade, upgraded, upgradeMsgId, upgradeStars, } = action; return { @@ -377,12 +379,13 @@ function buildApiMessageActionStarGift(action: GramJs.MessageActionStarGift) : A isNameHidden: Boolean(nameHidden), isSaved: Boolean(saved), isConverted: converted, - gift: buildApiStarGift(gift), + gift: buildApiStarGift(gift) as ApiStarGiftRegular, message: message && buildApiFormattedText(message), starsToConvert: convertStars?.toJSNumber(), canUpgrade, isUpgraded: upgraded, upgradeMsgId, + alreadyPaidUpgradeStars: upgradeStars?.toJSNumber(), }; } @@ -395,7 +398,7 @@ function buildApiMessageActionStarGiftUnique( return { type: 'starGiftUnique', - gift: buildApiStarGift(gift), + gift: buildApiStarGift(gift) as ApiStarGiftUnique, canExportAt, isRefunded: refunded, isSaved: saved, @@ -757,7 +760,7 @@ function buildAction( text = action.upgrade ? 'Notification.StarsGift.UpgradeYou' : 'ActionUniqueGiftTransferOutbound'; } else { text = action.upgrade ? 'Notification.StarsGift.Upgrade' : 'ActionUniqueGiftTransferInbound'; - translationValues.push('%action_origin%'); + translationValues.push('%action_origin_chat%'); } starGift = buildApiMessageActionStarGiftUnique(action); diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 71f9790fb..c5ad1f416 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -643,13 +643,14 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) { case 'stargift': { const { - user, shouldHideName, giftId, message, + user, shouldHideName, giftId, message, shouldUpgrade, } = invoice; return new GramJs.InputInvoiceStarGift({ userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, hideName: shouldHideName || undefined, giftId: BigInt(giftId), message: message && buildInputTextWithEntities(message), + includeUpgrade: shouldUpgrade, }); } @@ -673,6 +674,13 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) { }); } + case 'stargiftUpgrade': { + return new GramJs.InputInvoiceStarGiftUpgrade({ + msgId: invoice.messageId, + keepOriginalDetails: invoice.shouldKeepOriginalDetails, + }); + } + case 'giveaway': default: { const purpose = buildInputStorePaymentPurpose(invoice.purpose); diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index f0a92f5e5..c8302989e 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -14,6 +14,7 @@ import type { import { DEBUG } from '../../../config'; import { buildApiStarGift, + buildApiStarGiftAttribute, buildApiUserStarGift, } from '../apiBuilders/gifts'; import { @@ -646,9 +647,40 @@ export async function fetchUniqueStarGift({ slug }: { }) { const result = await invokeRequest(new GramJs.payments.GetUniqueStarGift({ slug })); + if (!result) return undefined; + + const gift = buildApiStarGift(result.gift); + if (gift.type !== 'starGiftUnique') return undefined; + return gift; +} + +export async function fetchStarGiftUpgradePreview({ + giftId, +}: { + giftId: string; +}) { + const result = await invokeRequest(new GramJs.payments.GetStarGiftUpgradePreview({ + giftId: BigInt(giftId), + })); + if (!result) { return undefined; } - const gift = buildApiStarGift(result.gift); - return gift.type === 'starGiftUnique' ? gift : undefined; + + return result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean); +} + +export function upgradeGift({ + messageId, + shouldKeepOriginalDetails, +}: { + messageId: number; + shouldKeepOriginalDetails?: true; +}) { + return invokeRequest(new GramJs.payments.UpgradeStarGift({ + msgId: messageId, + keepOriginalDetails: shouldKeepOriginalDetails, + }), { + shouldReturnTrue: true, + }); } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 54b75d691..d422e86f4 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -6,7 +6,7 @@ import type { ApiInputStorePaymentPurpose, ApiLabeledPrice, ApiPremiumGiftCodeOption, - ApiStarGift, + ApiStarGiftRegular, ApiStarGiftUnique, } from './payments'; import type { @@ -265,6 +265,7 @@ export type ApiInputInvoiceStarGift = { userId: string; giftId: string; message?: ApiFormattedText; + shouldUpgrade?: true; }; export type ApiInputInvoiceStarsGiveaway = { @@ -287,8 +288,14 @@ export type ApiInputInvoiceChatInviteSubscription = { hash: string; }; +export type ApiInputInvoiceStarGiftUpgrade = { + type: 'stargiftUpgrade'; + messageId: number; + shouldKeepOriginalDetails?: true; +}; + export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway -| ApiInputInvoiceGiftCode | ApiInputInvoiceStars | ApiInputInvoiceStarsGift +| ApiInputInvoiceGiftCode | ApiInputInvoiceStars | ApiInputInvoiceStarsGift | ApiInputInvoiceStarGiftUpgrade | ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription; /* Used for Invoice request */ @@ -325,6 +332,7 @@ export type ApiRequestInputInvoiceStarGift = { user: ApiUser; giftId: string; message?: ApiFormattedText; + shouldUpgrade?: true; }; export type ApiRequestInputInvoiceChatInviteSubscription = { @@ -332,9 +340,15 @@ export type ApiRequestInputInvoiceChatInviteSubscription = { hash: string; }; +export type ApiRequestInputInvoiceStarGiftUpgrade = { + type: 'stargiftUpgrade'; + messageId: number; + shouldKeepOriginalDetails?: true; +}; + export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug | ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway -| ApiRequestInputInvoiceChatInviteSubscription | ApiRequestInputInvoiceStarGift; +| ApiRequestInputInvoiceChatInviteSubscription | ApiRequestInputInvoiceStarGift | ApiRequestInputInvoiceStarGiftUpgrade; export interface ApiInvoice { prices: ApiLabeledPrice[]; @@ -464,12 +478,13 @@ export interface ApiMessageActionStarGift { isNameHidden: boolean; isSaved: boolean; isConverted?: true; - gift: ApiStarGift; + gift: ApiStarGiftRegular; message?: ApiFormattedText; starsToConvert?: number; canUpgrade?: true; isUpgraded?: true; upgradeMsgId?: number; + alreadyPaidUpgradeStars?: number; } export interface ApiMessageActionStarGiftUnique { @@ -478,7 +493,7 @@ export interface ApiMessageActionStarGiftUnique { isTransferred?: true; isSaved?: true; isRefunded?: true; - gift: ApiStarGift; + gift: ApiStarGiftUnique; canExportAt?: number; transferStars?: number; } diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index a40417592..33aa30298 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -216,6 +216,7 @@ export interface ApiStarGiftUnique { issuedCount: number; totalCount: number; attributes: ApiStarGiftAttribute[]; + slug: string; } export type ApiStarGift = ApiStarGiftRegular | ApiStarGiftUnique; @@ -264,6 +265,9 @@ export interface ApiUserStarGift { message?: ApiFormattedText; messageId?: number; starsToConvert?: number; + canUpgrade?: true; + alreadyPaidUpgradeStars?: number; + transferStars?: number; isConverted?: boolean; // Local field, used for Action Message upgradeMsgId?: number; // Local field, used for Action Message } diff --git a/src/assets/font-icons/auction.svg b/src/assets/font-icons/auction.svg new file mode 100644 index 000000000..dba1a6fa3 --- /dev/null +++ b/src/assets/font-icons/auction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/diamond.svg b/src/assets/font-icons/diamond.svg new file mode 100644 index 000000000..661ce9db9 --- /dev/null +++ b/src/assets/font-icons/diamond.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/trade.svg b/src/assets/font-icons/trade.svg new file mode 100644 index 000000000..86605005e --- /dev/null +++ b/src/assets/font-icons/trade.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 1e8d78473..df84dabdb 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1377,15 +1377,18 @@ "GiftInfoDescription_other" = "You can keep this gift in your Profile or convert it to **{amount}** Stars."; "GiftInfoDescriptionOut_one" = "{user} can keep this gift in profile or convert it to **{amount}** Star."; "GiftInfoDescriptionOut_other" = "{user} can keep this gift in profile or convert it to **{amount}** Stars."; +"GiftInfoDescriptionUpgrade_one" = "You can keep this gift, upgrade it, or sell it for **{amount}** Star."; +"GiftInfoDescriptionUpgrade_other" = "You can keep this gift, upgrade it, or sell it for **{amount}** Stars."; "GiftInfoDescriptionConverted_one" = "You converted this gift to **{amount}** Star."; "GiftInfoDescriptionConverted_other" = "You converted this gift to **{amount}** Stars."; "GiftInfoDescriptionOutConverted_one" = "{user} converted this gift to **{amount}** Star."; "GiftInfoDescriptionOutConverted_other" = "{user} converted this gift to **{amount}** Stars."; +"GiftInfoDescriptionFreeUpgrade" = "Upgrade this gift for free to turn it to a unique collectible."; +"GiftInfoDescriptionFreeUpgradeOut" = "{user} can turn this gift to a unique collectible"; +"GiftInfoDescriptionUpgraded" = "This gift was turned into a unique collectible."; "GiftInfoFrom" = "From"; "GiftInfoDate" = "Date"; "GiftInfoValue" = "Value"; -"GiftInfoMakeVisible" = "Display on my Page"; -"GiftInfoMakeInvisible" = "Hide from my Page"; "GiftInfoConvert_one" = "Convert to {amount} Star"; "GiftInfoConvert_other" = "Convert to {amount} Stars"; "GiftInfoConvertTitle" = "Convert Gift to Stars"; @@ -1394,8 +1397,9 @@ "GiftInfoConvertDescriptionPeriod_one" = "Conversion is available for the next **{count} days**."; "GiftInfoConvertDescriptionPeriod_other" = "Conversion is available for the next **{count} days**."; "GiftInfoSaved" = "This gift is visible on your profile. {link}"; -"GiftInfoSavedView" = "View >"; -"GiftInfoHidden" = "This gift is hidden. Only you can see it."; +"GiftInfoSavedHide" = "Hide >"; +"GiftInfoSavedShow" = "Show >"; +"GiftInfoHidden" = "This gift is hidden. Only you can see it. {link}"; "GiftInfoAvailability" = "Availability"; "GiftInfoAvailabilityValue_one" = "{count} of {total} left"; "GiftInfoAvailabilityValue_other" = "{count} of {total} left"; @@ -1418,6 +1422,25 @@ "GiftInfoStatus" = "Status"; "GiftInfoStatusNonUnique" = "Non-Unique"; "GiftInfoViewUpgraded" = "View Upgraded Gift"; +"GiftInfoUpgradeBadge" = "upgrade"; +"GiftInfoUpgradeForFree" = "Upgrade For Free"; +"GiftUpgradeUniqueTitle" = "Unique"; +"GiftUpgradeUniqueDescription" = "Turn your gift into a unique collectible that you can transfer or auction."; +"GiftUpgradeTransferableTitle" = "Transferable"; +"GiftUpgradeTransferableDescription" = "Send your upgraded gift to any of your friends on Telegram."; +"GiftUpgradeTradeableTitle" = "Tradeable"; +"GiftUpgradeTradeableDescription" = "Sell or auction your gift on third-party NFT marketplaces."; +"GiftUpgradeTitle" = "Make unique"; +"GiftUpgradeText" = "Let {peer} turn your gift into a unique collectible."; +"GiftUpgradeTextOwn" = "Turn your gift into a unique collectible that you can transfer or auction."; +"GiftUpgradeKeepDetails" = "Keep sender's name and comment"; +"GiftUpgradeButton" = "Upgrade {amount}"; +"GiftUpgradedTitle" = "This gift is now a collectible"; +"GiftUpgradedDescription" = "You have received a unique number, model, backdrop and symbol for your gift."; +"GiftMakeUnique" = "Make unique for {stars}"; +"GiftMakeUniqueAcc" = "Make unique"; +"GiftMakeUniqueDescription" = "Enable this to let {user} turn your gift into a unique collectible. {link}"; +"GiftMakeUniqueLink" = "Learn More >"; "StarsAmount" = "⭐️{amount}"; "StarsAmountText_one" = "{amount} Star"; "StarsAmountText_other" = "{amount} Stars"; @@ -1446,15 +1469,6 @@ "GiftFrom" = "Gift from"; "ReceivedGift" = "Received Gift"; "SentGift" = "Sent Gift"; -"StarGiftInfoDescriptionInbound" = "You can keep this gift in your Profile or convert it to {count} Stars. {link}"; -"StarGiftInfoDescriptionOutgoing" = "{user} can keep this gift in Profile or convert it to {count} Stars. {link}"; -"StarGiftInfoLinkCaption" = "More About Stars >"; -"StarGiftDisplayOnMyPage" = "Display on on my page"; -"StarGiftConvertTo" = "Convert to"; -"StarGiftHideFromMyPage" = "Hide from my page"; -"StarGiftSenderPrivacyNote" = "Only you can see the sender's name and message."; -"StarGiftAvailability" = "Availability"; -"StarGiftAvailabilityValue" = "{number} of {total} left"; "StarsSubscribeText_one" = "Do you want to subscribe to **{chat}** for **{amount} Star** per month?"; "StarsSubscribeText_other" = "Do you want to subscribe to **{chat}** for **{amount} Stars** per month?"; "StarsSubscribeInfo" = "By subscribing you agree to the {link}"; diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index 803faa177..5a61b1974 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -8,3 +8,4 @@ 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 GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal'; diff --git a/src/components/common/AboutMonetizationModal.tsx b/src/components/common/AboutMonetizationModal.tsx index 560fef02b..b4088779a 100644 --- a/src/components/common/AboutMonetizationModal.tsx +++ b/src/components/common/AboutMonetizationModal.tsx @@ -94,6 +94,7 @@ const AboutMonetizationModal: FC = ({ isOpen={isOpen} listItemData={modalData.listItemData} headerIconName="cash-circle" + withSeparator header={modalData.header} footer={modalData.footer} buttonText={oldLang('RevenueSharingAdsUnderstood')} diff --git a/src/components/common/AnimatedIconFromSticker.tsx b/src/components/common/AnimatedIconFromSticker.tsx index 40d761421..406d6b22e 100644 --- a/src/components/common/AnimatedIconFromSticker.tsx +++ b/src/components/common/AnimatedIconFromSticker.tsx @@ -20,7 +20,7 @@ function AnimatedIconFromSticker(props: OwnProps) { } = props; const thumbDataUri = sticker?.thumbnail?.dataUri; - const localMediaHash = sticker && `sticker${sticker.id}`; + const localMediaHash = sticker && getStickerMediaHash(sticker, 'full'); const previewBlobUrl = useMedia( sticker ? getStickerMediaHash(sticker, 'preview') : undefined, noLoad && !forcePreview, diff --git a/src/components/common/AnimatedIconWithPreview.module.scss b/src/components/common/AnimatedIconWithPreview.module.scss index b124c6825..4faaa222f 100644 --- a/src/components/common/AnimatedIconWithPreview.module.scss +++ b/src/components/common/AnimatedIconWithPreview.module.scss @@ -16,6 +16,6 @@ height: 100%; &:global(.closing) { - transition-delay: 150ms; + transition-delay: 50ms; } } diff --git a/src/components/common/helpers/gifts.ts b/src/components/common/helpers/gifts.ts index c784e79be..e08318a21 100644 --- a/src/components/common/helpers/gifts.ts +++ b/src/components/common/helpers/gifts.ts @@ -1,6 +1,7 @@ import type { ApiFormattedText, ApiStarGift, + ApiStarGiftAttribute, ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributeOriginalDetails, @@ -40,10 +41,14 @@ export function getGiftMessage(gift: ApiStarGift): ApiFormattedText | undefined export function getGiftAttributes(gift: ApiStarGift): GiftAttributes | undefined { if (gift.type !== 'starGiftUnique') return undefined; - const model = gift.attributes.find((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model'); - const backdrop = gift.attributes.find((attr): attr is ApiStarGiftAttributeBackdrop => attr.type === 'backdrop'); - const pattern = gift.attributes.find((attr): attr is ApiStarGiftAttributePattern => attr.type === 'pattern'); - const originalDetails = gift.attributes.find((attr): attr is ApiStarGiftAttributeOriginalDetails => ( + return getGiftAttributesFromList(gift.attributes); +} + +export function getGiftAttributesFromList(attributes: ApiStarGiftAttribute[]) { + const model = attributes.find((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model'); + const backdrop = attributes.find((attr): attr is ApiStarGiftAttributeBackdrop => attr.type === 'backdrop'); + const pattern = attributes.find((attr): attr is ApiStarGiftAttributePattern => attr.type === 'pattern'); + const originalDetails = attributes.find((attr): attr is ApiStarGiftAttributeOriginalDetails => ( attr.type === 'originalDetails' )); diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index a48aa6b46..d9f4bc723 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -95,7 +95,7 @@ export function renderActionMessageText( } if (translationKey.startsWith('Notification.StarsGift.Upgrade')) { unprocessed = unprocessed - .replace('%@', '%action_origin%'); + .replace('%@', '%action_origin_chat%'); } if (translationKey.startsWith('ActionUniqueGiftTransfer')) { unprocessed = unprocessed @@ -138,6 +138,18 @@ export function renderActionMessageText( unprocessed = processed.pop() as string; content.push(...processed); + processed = processPlaceholder( + unprocessed, + '%action_origin_chat%', + actionOriginChat ? ( + renderChatContent(oldLang, actionOriginChat, noLinks) || NBSP + ) : 'Chat', + '', + ); + + unprocessed = processed.pop() as string; + content.push(...processed); + if (unprocessed.includes('%payment_amount%')) { processed = processPlaceholder( unprocessed, diff --git a/src/components/common/profile/RadialPatternBackground.tsx b/src/components/common/profile/RadialPatternBackground.tsx index 25d26337f..1ac3407e3 100644 --- a/src/components/common/profile/RadialPatternBackground.tsx +++ b/src/components/common/profile/RadialPatternBackground.tsx @@ -1,5 +1,5 @@ import React, { - memo, useEffect, useRef, useSignal, useState, + memo, useEffect, useMemo, useRef, useSignal, useState, } from '../../../lib/teact/teact'; import type { ApiSticker } from '../../../api/types'; @@ -22,6 +22,7 @@ type OwnProps = { patternColor?: string; patternIcon?: ApiSticker; className?: string; + clearBottomSector?: boolean; }; const RINGS = 3; @@ -33,38 +34,11 @@ const BASE_ICON_SIZE = 20; const MIN_SIZE = 250; -const PATTERN_POSITIONS = (() => { - const coordinates: { x: number; y: number; sizeFactor: number }[] = []; - for (let ring = 1; ring <= RINGS; ring++) { - const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT)); - const ringProgress = ring / RINGS; - const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress; - - const angleShift = ring % 2 === 0 ? Math.PI / ringItemCount : 0; - - for (let i = 0; i < ringItemCount; i++) { - const angle = (i / ringItemCount) * Math.PI * 2 + angleShift; - // Slightly oval - const xOffset = ringRadius * 1.71 * Math.cos(angle); - const yOffset = ringRadius * Math.sin(angle); - - const x = 0.5 + xOffset; - const y = 0.5 + yOffset; - - const sizeFactor = 1.4 - ringProgress * Math.random(); - - coordinates.push({ - x, y, sizeFactor, - }); - } - } - return coordinates; -})(); - const RadialPatternBackground = ({ backgroundColors, patternColor, patternIcon, + clearBottomSector, className, }: OwnProps) => { // eslint-disable-next-line no-null/no-null @@ -86,6 +60,39 @@ const RadialPatternBackground = ({ preloadImage(previewUrl).then(setEmojiImage); }, [previewUrl]); + const patternPositions = useMemo(() => { + const coordinates: { x: number; y: number; sizeFactor: number }[] = []; + for (let ring = 1; ring <= RINGS; ring++) { + const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT)); + const ringProgress = ring / RINGS; + const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress; + + const angleShift = ring % 2 === 0 ? Math.PI / ringItemCount : 0; + + for (let i = 0; i < ringItemCount; i++) { + const angle = (i / ringItemCount) * Math.PI * 2 + angleShift; + + if (clearBottomSector && angle > Math.PI * 0.45 && angle < Math.PI * 0.55) { + continue; + } + + // Slightly oval + const xOffset = ringRadius * 1.71 * Math.cos(angle); + const yOffset = ringRadius * Math.sin(angle); + + const x = 0.5 + xOffset; + const y = 0.5 + yOffset; + + const sizeFactor = 1.4 - ringProgress * Math.random(); + + coordinates.push({ + x, y, sizeFactor, + }); + } + } + return coordinates; + }, [clearBottomSector]); + useResizeObserver(containerRef, (entry) => { setContainerSize({ width: entry.contentRect.width, @@ -111,7 +118,7 @@ const RadialPatternBackground = ({ if (!width || !height) return; ctx.save(); - PATTERN_POSITIONS.forEach(({ + patternPositions.forEach(({ x, y, sizeFactor, }) => { const centerShift = (width - Math.max(width, MIN_SIZE * dpr)) / 2; // Shift coords if canvas is smaller than `MIN_SIZE` diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index 412ad348d..fd48a668a 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -4,7 +4,7 @@ import React, { import { getGlobal } from '../../../../global'; import type { - ApiChat, ApiDraft, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser, + ApiChat, ApiDraft, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, StatefulMediaContent, } from '../../../../api/types'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; @@ -21,10 +21,9 @@ import { getMessageSticker, getMessageVideo, isActionMessage, - isChatChannel, - isChatGroup, isExpiredMessage, } from '../../../../global/helpers'; +import { isApiPeerChat } from '../../../../global/helpers/peers'; import { getMessageReplyInfo } from '../../../../global/helpers/replies'; import buildClassName from '../../../../util/buildClassName'; import { renderActionMessageText } from '../../../common/helpers/renderActionMessageText'; @@ -155,15 +154,13 @@ export default function useChatListEntry({ } if (isAction) { - const isChat = chat && (isChatChannel(chat) || isChatGroup(chat)); - return (

{renderActionMessageText( oldLang, lastMessage, - !isChat ? lastMessageSender as ApiUser : undefined, - isChat ? chat : undefined, + lastMessageSender && !isApiPeerChat(lastMessageSender) ? lastMessageSender : undefined, + lastMessageSender && isApiPeerChat(lastMessageSender) ? lastMessageSender : chat, actionTargetUsers, actionTargetMessage, actionTargetChatId, diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index 0fc926b67..35571b1c2 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -128,7 +128,6 @@ const ActionMessage: FC = ({ getReceipt, openGiftInfoModalFromMessage, openPrizeStarsTransactionFromGiveaway, - showNotification, } = getActions(); const oldLang = useOldLang(); @@ -240,16 +239,6 @@ const ActionMessage: FC = ({ const handleStarGiftClick = () => { const starGift = message.content.action?.starGift; if (!starGift) return; - if (starGift.type === 'starGift' && starGift.canUpgrade && !message.isOutgoing) { - showNotification({ - title: { - key: 'ActionUnsupportedTitle', - }, - message: { - key: 'ActionUnsupportedDescription', - }, - }); - } openGiftInfoModalFromMessage({ chatId: message.chatId, @@ -534,7 +523,8 @@ const ActionMessage: FC = ({

- {starGift.canUpgrade && !message.isOutgoing ? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')} + {starGift.alreadyPaidUpgradeStars && (!message.isOutgoing || targetUsers?.[0]?.isSelf) + ? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')}
{starGift.gift.availabilityTotal && ( = ({ const backgroundColors = [backdrop.centerColor, backdrop.edgeColor]; + const adaptedPatternColor = `${backdrop.patternColor.slice(0, 7)}55`; + return (
@@ -571,6 +564,7 @@ const ActionMessage: FC = ({ backgroundColors={backgroundColors} patternColor={backdrop.patternColor} patternIcon={pattern.sticker} + clearBottomSector />
= ({ {oldLang('Gift2UniqueView')}
diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx index f5e3f6ef5..72941dddf 100644 --- a/src/components/middle/message/WebPage.tsx +++ b/src/components/middle/message/WebPage.tsx @@ -147,8 +147,10 @@ const WebPage: FC = ({ const isStory = type === WEBPAGE_STORY_TYPE; const isGift = type === WEBPAGE_GIFT_TYPE; const isExpiredStory = story && 'isDeleted' in story; + const quickButtonLangKey = !inPreview && !isExpiredStory ? getWebpageButtonLangKey(type) : undefined; const quickButtonTitle = quickButtonLangKey && lang(quickButtonLangKey); + const truncatedDescription = trimText(description, MAX_TEXT_LENGTH); const isArticle = Boolean(truncatedDescription || title || siteName); let isSquarePhoto = Boolean(stickers); @@ -185,6 +187,7 @@ const WebPage: FC = ({ size="tiny" color="translucent" isRectangular + noForcedUpperCase onClick={handleOpenTelegramLink} > {caption} diff --git a/src/components/middle/message/helpers/webpageType.ts b/src/components/middle/message/helpers/webpageType.ts index 0ee1032bc..efe547f16 100644 --- a/src/components/middle/message/helpers/webpageType.ts +++ b/src/components/middle/message/helpers/webpageType.ts @@ -1,5 +1,7 @@ +import type { RegularLangKey } from '../../../../types/language'; + // https://github.com/telegramdesktop/tdesktop/blob/3da787791f6d227f69b32bf4003bc6071d05e2ac/Telegram/SourceFiles/history/view/history_view_view_button.cpp#L51 -export function getWebpageButtonLangKey(type?: string) { +export function getWebpageButtonLangKey(type?: string): RegularLangKey | undefined { switch (type) { case 'telegram_channel_request': case 'telegram_megagroup_request': diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 6f5d9116f..47d150da7 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -17,6 +17,7 @@ import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.a import PremiumGiftModal from './gift/GiftModal.async'; import GiftInfoModal from './gift/info/GiftInfoModal.async'; import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async'; +import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async'; import GiftCodeModal from './giftcode/GiftCodeModal.async'; import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async'; import LocationAccessModal from './locationAccess/LocationAccessModal.async'; @@ -63,7 +64,8 @@ type ModalKey = keyof Pick; type StateProps = { @@ -106,6 +108,7 @@ const MODALS: ModalRegistry = { emojiStatusAccessModal: EmojiStatusAccessModal, locationAccessModal: LocationAccessModal, aboutAdsModal: AboutAdsModal, + giftUpgradeModal: GiftUpgradeModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/modals/aboutAds/AboutAdsModal.module.scss b/src/components/modals/aboutAds/AboutAdsModal.module.scss index 0085e9390..e514420c3 100644 --- a/src/components/modals/aboutAds/AboutAdsModal.module.scss +++ b/src/components/modals/aboutAds/AboutAdsModal.module.scss @@ -10,8 +10,8 @@ .moreButton { position: absolute; - top: 0.5rem; - right: 0.5rem; + top: 0.375rem; + right: 0.375rem; } .secondary { diff --git a/src/components/modals/aboutAds/AboutAdsModal.tsx b/src/components/modals/aboutAds/AboutAdsModal.tsx index e64a8c72f..0360c308c 100644 --- a/src/components/modals/aboutAds/AboutAdsModal.tsx +++ b/src/components/modals/aboutAds/AboutAdsModal.tsx @@ -133,6 +133,7 @@ const AboutAdsModal = ({ message, minLevelToRestrictAds }: OwnProps & StateProps isOpen={isOpen} listItemData={modalData?.listItemData} headerIconName="channel" + withSeparator header={modalData?.header} footer={modalData?.footer} buttonText={oldLang('RevenueSharingAdsUnderstood')} diff --git a/src/components/modals/common/TableAboutModal.module.scss b/src/components/modals/common/TableAboutModal.module.scss index d9a65797d..7d9b0b148 100644 --- a/src/components/modals/common/TableAboutModal.module.scss +++ b/src/components/modals/common/TableAboutModal.module.scss @@ -1,5 +1,6 @@ .root :global(.modal-dialog) { width: 26.25rem; + overflow: hidden; } .title, .description { diff --git a/src/components/modals/common/TableAboutModal.tsx b/src/components/modals/common/TableAboutModal.tsx index 3e9bc30bd..283d7e109 100644 --- a/src/components/modals/common/TableAboutModal.tsx +++ b/src/components/modals/common/TableAboutModal.tsx @@ -15,10 +15,12 @@ export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][]; type OwnProps = { isOpen?: boolean; listItemData?: TableAboutData; - headerIconName: IconName; + headerIconName?: IconName; header?: TeactNode; footer?: TeactNode; buttonText?: string; + hasBackdrop?: boolean; + withSeparator?: boolean; onClose: NoneToVoidFunction; onButtonClick?: NoneToVoidFunction; }; @@ -30,6 +32,8 @@ const TableAboutModal = ({ header, footer, buttonText, + hasBackdrop, + withSeparator, onClose, onButtonClick, }: OwnProps) => { @@ -38,9 +42,11 @@ const TableAboutModal = ({ isOpen={isOpen} className={styles.root} contentClassName={styles.content} + hasAbsoluteCloseButton + absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined} onClose={onClose} > -
+ {headerIconName &&
} {header}
{listItemData?.map(([icon, title, subtitle]) => { @@ -56,7 +62,7 @@ const TableAboutModal = ({ ); })}
- + {withSeparator && } {footer} {buttonText && ( diff --git a/src/components/modals/gift/GiftComposer.module.scss b/src/components/modals/gift/GiftComposer.module.scss index d949f1ca2..92ce8f90a 100644 --- a/src/components/modals/gift/GiftComposer.module.scss +++ b/src/components/modals/gift/GiftComposer.module.scss @@ -1,9 +1,15 @@ +@use "../../../styles/mixins"; + .root { height: 100%; display: flex; flex-direction: column; overflow-y: auto; + overflow-x: hidden; padding-top: 3.5rem; + padding-inline: 0.75rem; + + @include mixins.adapt-padding-to-scrollbar(0.75rem); } .header { @@ -47,9 +53,9 @@ } .optionsSection { - padding: 1rem; + padding-top: 1rem; padding-bottom: 0.5rem; - box-shadow: 0 1px 2px var(--color-default-shadow); + padding-inline: 0.25rem; } .checkboxTitle { @@ -69,6 +75,9 @@ overflow: hidden; flex: 0 0 auto; + border-bottom-right-radius: var(--border-radius-default); + border-bottom-left-radius: var(--border-radius-default); + background-color: var(--theme-background-color); background-position: center; background-repeat: no-repeat; @@ -112,18 +121,28 @@ justify-content: space-between; padding: 1rem; padding-top: 0.5rem; + margin-inline: -0.75rem; // Account for padding flex-grow: 1; flex-direction: column; background-color: var(--color-background-secondary); + + position: sticky; + bottom: 0; } .switcher { - margin-bottom: 0 !important; + margin-bottom: 0.25rem; +} + +.switcherStarIcon { + margin-inline: 0 !important; } .description { color: var(--color-text-secondary); font-size: 0.875rem; + margin-bottom: 0.5rem; + margin-left: 1rem; } .main-button { diff --git a/src/components/modals/gift/GiftComposer.tsx b/src/components/modals/gift/GiftComposer.tsx index e8a2e0a32..cf0a78587 100644 --- a/src/components/modals/gift/GiftComposer.tsx +++ b/src/components/modals/gift/GiftComposer.tsx @@ -23,6 +23,7 @@ import useLastCallback from '../../../hooks/useLastCallback'; import PremiumProgress from '../../common/PremiumProgress'; import ActionMessage from '../../middle/ActionMessage'; import Button from '../../ui/Button'; +import Link from '../../ui/Link'; import ListItem from '../../ui/ListItem'; import Switcher from '../../ui/Switcher'; import TextArea from '../../ui/TextArea'; @@ -61,12 +62,13 @@ function GiftComposer({ currentUserId, isPaymentFormLoading, }: OwnProps & StateProps) { - const { sendStarGift, openInvoice } = getActions(); + const { sendStarGift, openInvoice, openGiftUpgradeModal } = getActions(); const lang = useLang(); const [giftMessage, setGiftMessage] = useState(''); const [shouldHideName, setShouldHideName] = useState(false); + const [shouldPayForUpgrade, setShouldPayForUpgrade] = useState(false); const customBackgroundValue = useCustomBackground(theme, customBackground); @@ -119,6 +121,7 @@ function GiftComposer({ } : undefined, isNameHidden: shouldHideName, starsToConvert: gift.starsToConvert, + canUpgrade: shouldPayForUpgrade || undefined, isSaved: false, gift, }, @@ -126,7 +129,7 @@ function GiftComposer({ }, }, } satisfies ApiMessage; - }, [currentUserId, gift, giftMessage, isStarGift, shouldHideName, userId]); + }, [currentUserId, gift, giftMessage, isStarGift, shouldHideName, shouldPayForUpgrade, userId]); const handleGiftMessageChange = useLastCallback((e: ChangeEvent) => { setGiftMessage(e.target.value); @@ -136,6 +139,18 @@ function GiftComposer({ setShouldHideName(!shouldHideName); }); + const handleShouldPayForUpgradeChange = useLastCallback(() => { + setShouldPayForUpgrade(!shouldPayForUpgrade); + }); + + const handleOpenUpgradePreview = useLastCallback(() => { + if (!isStarGift) return; + openGiftUpgradeModal({ + giftId: gift.id, + peerId: userId, + }); + }); + const handleMainButtonClick = useLastCallback(() => { if (isStarGift) { sendStarGift({ @@ -143,6 +158,7 @@ function GiftComposer({ shouldHideName, gift, message: giftMessage ? { text: giftMessage } : undefined, + shouldUpgrade: shouldPayForUpgrade, }); return; } @@ -159,6 +175,8 @@ function GiftComposer({ function renderOptionsSection() { const symbolsLeft = captionLimit ? captionLimit - giftMessage.length : undefined; + + const userFullName = getUserFullName(user)!; return (