Star Gifts: Support upgrade option (#5475)

This commit is contained in:
zubiden 2025-01-21 18:21:59 +01:00 committed by Alexander Zinchuk
parent 8bac803dea
commit aecd1d216f
63 changed files with 1344 additions and 530 deletions

View File

@ -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

View File

@ -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(),
};
}

View File

@ -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);

View File

@ -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);

View File

@ -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,
});
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path d="M5.274 24.224c-1.958 0-3.575 1.616-3.575 3.575v1.1a1.375 1.375 0 0 0 1.375 1.376A1.375 1.375 0 0 0 4.45 28.9v-1.101c0-.473.353-.825.825-.825h8.8c.472 0 .825.352.825.825v1.1a1.375 1.375 0 0 0 1.375 1.376 1.375 1.375 0 0 0 1.375-1.375v-1.101c0-1.959-1.617-3.575-3.575-3.575z" style="color:#000;fill:#000;stroke-width:1.375;stroke-linecap:round;-inkscape-stroke:none"/><path d="M19.28 1.727c-.778 0-1.557.292-2.141.877a3.03 3.03 0 0 0-.864 2.42L9.45 11.851a3.018 3.018 0 0 0-2.422.863c-1.168 1.168-1.168 3.109 0 4.277l5.448 5.445c1.168 1.169 3.107 1.169 4.275 0 .654-.653.94-1.549.863-2.416l.889-.888 6.807 6.805a3.59 3.59 0 0 0 5.052 0c1.385-1.385 1.385-3.67 0-5.055l-6.802-6.807.886-.887a3.02 3.02 0 0 0 2.416-.863c1.169-1.168 1.17-3.109.002-4.277l-5.445-5.447a3 3 0 0 0-2.139-.875m0 2.726q.098 0 .193.094l5.447 5.445c.124.125.123.264-.002.389-.123.123-.26.123-.383.004l-.006-.004-5.445-5.445v-.002c-.123-.125-.124-.261 0-.385.062-.062.13-.095.195-.096m-1.555 3.012 4.277 4.28-.389.386v.002l-.972.97-4.086 4.085.002.002-.387.386-4.28-4.277zm-8.559 7.1q.1-.002.195.093l5.448 5.446v.002c.123.124.122.264-.002.388-.125.125-.265.125-.39 0l-5.444-5.447c-.125-.125-.125-.262 0-.387q.093-.094.193-.096m12.447 1.456 6.805 6.807a.8.8 0 0 1 0 1.164.804.804 0 0 1-1.166 0l-6.805-6.805Z" style="color:#000;fill:#000;stroke-width:1.375;-inkscape-stroke:none"/><path d="M1.973 27.522a1.375 1.375 0 0 0-1.375 1.375 1.375 1.375 0 0 0 1.375 1.375h15.402a1.375 1.375 0 0 0 1.375-1.375 1.375 1.375 0 0 0-1.375-1.375z" style="color:#000;fill:#000;stroke-width:1.375;stroke-linecap:round;-inkscape-stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path d="M8.332 2.201a3.58 3.58 0 0 0-2.908 1.496L1.525 9.152a3.59 3.59 0 0 0 .07 4.25L13.16 28.525c1.41 1.844 4.27 1.844 5.68 0l11.564-15.123c.95-1.243.98-2.976.07-4.25l-3.898-5.455a3.58 3.58 0 0 0-2.908-1.496H15.25Zm0 2.75h3.383L9.754 9.525H4.639l3.021-4.23a.82.82 0 0 1 .672-.344m6.918 0h1.5c.334 0 .626.192.758.498l1.746 4.076h-6.508l1.746-4.076a.815.815 0 0 1 .758-.498m5.035 0h3.383a.82.82 0 0 1 .672.344l3.021 4.23h-5.115Zm-16.09 7.324h5.172q.017.074.037.147l3.186 10.832zm8.034 0h7.542L16 25.104Zm10.404 0h5.172L19.41 23.254l3.186-10.832q.02-.074.037-.147" style="color:#000;fill:#000;-inkscape-stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path d="M21.357 3.584q-.472 0-1.037.002h-3.24a1.35 1.35 0 0 0-1.35 1.35 1.35 1.35 0 0 0 1.35 1.35h3.24c1.512 0 2.08.07 2.233.148.254.13.46.335.59.59.077.152.146.72.146 2.232v1.058l-.934-.933a1.35 1.35 0 0 0-1.91 0 1.35 1.35 0 0 0 0 1.91l3.24 3.24a1.4 1.4 0 0 0 .256.198 1 1 0 0 0 .12.056 1.4 1.4 0 0 0 .183.076 1 1 0 0 0 .131.03 1.4 1.4 0 0 0 .205.027l.06.008.06-.008a1.4 1.4 0 0 0 .218-.03 1 1 0 0 0 .111-.025 1.4 1.4 0 0 0 .215-.09l.088-.043a1.4 1.4 0 0 0 .22-.168l.042-.03.008-.009 3.232-3.232a1.35 1.35 0 0 0 0-1.91 1.35 1.35 0 0 0-1.908 0l-.936.935v-1.06c0-1.512.07-2.457-.441-3.46a4.05 4.05 0 0 0-1.77-1.769c-.751-.383-1.471-.439-2.422-.443M7.209 16.555a1.35 1.35 0 0 0-.803.386l-3.24 3.24a1.35 1.35 0 0 0 0 1.909 1.35 1.35 0 0 0 1.908 0l.936-.936v1.063c0 1.512-.07 2.455.441 3.457a4.05 4.05 0 0 0 1.77 1.771c1.002.511 1.947.442 3.459.442h5.4a1.35 1.35 0 0 0 1.35-1.352 1.35 1.35 0 0 0-1.35-1.35h-5.4c-1.512 0-2.08-.068-2.233-.146a1.35 1.35 0 0 1-.59-.59c-.077-.153-.146-.72-.146-2.232v-1.06l.934.933a1.35 1.35 0 0 0 1.91 0 1.35 1.35 0 0 0 0-1.908l-3.24-3.24a1.35 1.35 0 0 0-1.106-.387" style="color:#000;fill:#000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"/><path d="M4.26 2.506c-.784 0-1.521.38-1.977 1.017L.785 5.621a2.44 2.44 0 0 0 .047 2.887L5.43 14.52c.951 1.245 2.91 1.245 3.861 0l4.596-6.011v-.002a2.44 2.44 0 0 0 .047-2.887l-1.496-2.098a2.44 2.44 0 0 0-1.979-1.017Zm.14 2.701h5.92l1.301 1.82-4.262 5.575-4.261-5.575ZM21.541 16.547c-.783 0-1.523.378-1.979 1.015l-1.496 2.098a2.44 2.44 0 0 0 .047 2.889l4.596 6.012c.952 1.244 2.91 1.244 3.861 0l4.598-6.012a2.44 2.44 0 0 0 .047-2.889l-1.498-2.097a2.43 2.43 0 0 0-1.977-1.016zm.139 2.7h5.92l1.302 1.82-4.261 5.574-4.262-5.575z" style="color:#000;fill:#000;-inkscape-stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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}";

View File

@ -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';

View File

@ -94,6 +94,7 @@ const AboutMonetizationModal: FC<OwnProps> = ({
isOpen={isOpen}
listItemData={modalData.listItemData}
headerIconName="cash-circle"
withSeparator
header={modalData.header}
footer={modalData.footer}
buttonText={oldLang('RevenueSharingAdsUnderstood')}

View File

@ -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,

View File

@ -16,6 +16,6 @@
height: 100%;
&:global(.closing) {
transition-delay: 150ms;
transition-delay: 50ms;
}
}

View File

@ -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'
));

View File

@ -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,

View File

@ -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`

View File

@ -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 (
<p className="last-message shared-canvas-container" dir={oldLang.isRtl ? 'auto' : 'ltr'}>
{renderActionMessageText(
oldLang,
lastMessage,
!isChat ? lastMessageSender as ApiUser : undefined,
isChat ? chat : undefined,
lastMessageSender && !isApiPeerChat(lastMessageSender) ? lastMessageSender : undefined,
lastMessageSender && isApiPeerChat(lastMessageSender) ? lastMessageSender : chat,
actionTargetUsers,
actionTargetMessage,
actionTargetChatId,

View File

@ -128,7 +128,6 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
getReceipt,
openGiftInfoModalFromMessage,
openPrizeStarsTransactionFromGiveaway,
showNotification,
} = getActions();
const oldLang = useOldLang();
@ -240,16 +239,6 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
<div className="action-message-button">
<Sparkles preset="button" />
{starGift.canUpgrade && !message.isOutgoing ? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')}
{starGift.alreadyPaidUpgradeStars && (!message.isOutgoing || targetUsers?.[0]?.isSelf)
? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')}
</div>
{starGift.gift.availabilityTotal && (
<GiftRibbon
@ -558,11 +548,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const backgroundColors = [backdrop.centerColor, backdrop.edgeColor];
const adaptedPatternColor = `${backdrop.patternColor.slice(0, 7)}55`;
return (
<span
className="action-message-gift action-message-centered action-message-unique"
tabIndex={0}
role="button"
style={`--pattern-color: ${adaptedPatternColor}`}
onClick={handleStarGiftClick}
>
<div className="action-message-unique-background-wrapper">
@ -571,6 +564,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
backgroundColors={backgroundColors}
patternColor={backdrop.patternColor}
patternIcon={pattern.sticker}
clearBottomSector
/>
</div>
<AnimatedIconFromSticker
@ -610,7 +604,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
{oldLang('Gift2UniqueView')}
</div>
<GiftRibbon
color={backdrop.patternColor || 'blue'}
color={adaptedPatternColor}
text={oldLang('ActionStarGift')}
/>
</span>

View File

@ -147,8 +147,10 @@ const WebPage: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
size="tiny"
color="translucent"
isRectangular
noForcedUpperCase
onClick={handleOpenTelegramLink}
>
{caption}

View File

@ -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':

View File

@ -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<TabState,
'suggestedStatusModal' |
'emojiStatusAccessModal' |
'locationAccessModal' |
'aboutAdsModal'
'aboutAdsModal' |
'giftUpgradeModal'
>;
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<ModalRegistry>;

View File

@ -10,8 +10,8 @@
.moreButton {
position: absolute;
top: 0.5rem;
right: 0.5rem;
top: 0.375rem;
right: 0.375rem;
}
.secondary {

View File

@ -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')}

View File

@ -1,5 +1,6 @@
.root :global(.modal-dialog) {
width: 26.25rem;
overflow: hidden;
}
.title, .description {

View File

@ -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}
>
<div className={styles.topIcon}><Icon name={headerIconName} /></div>
{headerIconName && <div className={styles.topIcon}><Icon name={headerIconName} /></div>}
{header}
<div>
{listItemData?.map(([icon, title, subtitle]) => {
@ -56,7 +62,7 @@ const TableAboutModal = ({
);
})}
</div>
<Separator className={styles.separator} />
{withSeparator && <Separator className={styles.separator} />}
{footer}
{buttonText && (
<Button size="smaller" onClick={onButtonClick || onClose}>{buttonText}</Button>

View File

@ -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 {

View File

@ -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<string>('');
const [shouldHideName, setShouldHideName] = useState<boolean>(false);
const [shouldPayForUpgrade, setShouldPayForUpgrade] = useState<boolean>(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<HTMLTextAreaElement>) => {
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 (
<div className={styles.optionsSection}>
<TextArea
@ -170,6 +188,31 @@ function GiftComposer({
maxLengthIndicator={symbolsLeft && symbolsLeft < LIMIT_DISPLAY_THRESHOLD ? symbolsLeft.toString() : undefined}
/>
{isStarGift && gift.upgradeStars && (
<ListItem className={styles.switcher} narrow ripple onClick={handleShouldPayForUpgradeChange}>
<span>
{lang('GiftMakeUnique', {
stars: formatStarsAsIcon(lang, gift.upgradeStars, { className: styles.switcherStarIcon }),
}, { withNodes: true })}
</span>
<Switcher
checked={shouldPayForUpgrade}
onChange={handleShouldPayForUpgradeChange}
label={lang('GiftMakeUniqueAcc')}
/>
</ListItem>
)}
{isStarGift && (
<div className={styles.description}>
{lang('GiftMakeUniqueDescription', {
user: userFullName,
link: <Link isPrimary onClick={handleOpenUpgradePreview}>{lang('GiftMakeUniqueLink')}</Link>,
}, {
withNodes: true,
})}
</div>
)}
{isStarGift && (
<ListItem className={styles.switcher} narrow ripple onClick={handleShouldHideNameChange}>
<span>{lang('GiftHideMyName')}</span>
@ -180,27 +223,22 @@ function GiftComposer({
/>
</ListItem>
)}
</div>
);
}
function renderFooter() {
const userFullName = getUserFullName(user)!;
const amount = isStarGift
? formatStarsAsIcon(lang, gift.stars, true)
: formatCurrency(gift.amount, gift.currency);
return (
<div className={styles.footer}>
{isStarGift && (
<div className={styles.description}>
{lang('GiftHideNameDescription', { profile: userFullName, receiver: userFullName })}
</div>
)}
</div>
);
}
<div className={styles.spacer} />
function renderFooter() {
const amount = isStarGift
? formatStarsAsIcon(lang, gift.stars + (shouldPayForUpgrade ? gift.upgradeStars! : 0), { asFont: true })
: formatCurrency(gift.amount, gift.currency);
return (
<div className={styles.footer}>
{isStarGift && gift.availabilityRemains && (
<PremiumProgress
isPrimary
@ -236,7 +274,7 @@ function GiftComposer({
);
return (
<div className={buildClassName(styles.root, 'no-scroll')}>
<div className={buildClassName(styles.root, 'custom-scroll')}>
<div
className={buildClassName(styles.actionMessageView, 'MessageList')}
// @ts-ignore -- FIXME: Find a way to disable interactions but keep a11y
@ -253,6 +291,7 @@ function GiftComposer({
<ActionMessage key={isStarGift ? gift.id : gift.months} message={localMessage} />
</div>
{renderOptionsSection()}
<div className={styles.spacer} />
{renderFooter()}
</div>
);

View File

@ -47,7 +47,7 @@ export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGiftRegular;
type StateProps = {
boostPerSentGift?: number;
starGiftsById?: Record<string, ApiStarGiftRegular>;
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
starGiftIdsByCategory?: Record<StarGiftCategory, string[]>;
starBalance?: ApiStarsAmount;
user?: ApiUser;
isSelf?: boolean;
@ -59,7 +59,7 @@ const INTERSECTION_THROTTLE = 200;
const PremiumGiftModal: FC<OwnProps & StateProps> = ({
modal,
starGiftsById,
starGiftCategoriesByName,
starGiftIdsByCategory,
starBalance,
user,
isSelf,
@ -206,7 +206,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
function renderStarGifts() {
return (
<div className={styles.starGiftsContainer}>
{starGiftsById && starGiftCategoriesByName[selectedCategory].map((giftId) => {
{starGiftsById && starGiftIdsByCategory?.[selectedCategory].map((giftId) => {
const gift = starGiftsById[giftId];
return (
<GiftItemStar
@ -331,8 +331,7 @@ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
const {
starGiftsById,
starGiftCategoriesByName,
starGifts,
stars,
currentUserId,
} = global;
@ -342,8 +341,8 @@ export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
return {
boostPerSentGift: global.appConfig?.boostsPerSentGift,
starGiftsById,
starGiftCategoriesByName,
starGiftsById: starGifts?.byId,
starGiftIdsByCategory: starGifts?.idsByCategory,
starBalance: stars?.balance,
user,
isSelf,

View File

@ -19,22 +19,22 @@ type OwnProps = {
};
type StateProps = {
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
idsByCategory?: Record<StarGiftCategory, string[]>;
};
const StarGiftCategoryList = ({
starGiftCategoriesByName,
idsByCategory,
onCategoryChanged,
}: StateProps & OwnProps) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const lang = useLang();
const starCategories: number[] = useMemo(() => Object.keys(starGiftCategoriesByName)
const starCategories: number[] | undefined = useMemo(() => idsByCategory && Object.keys(idsByCategory)
.filter((category) => category !== 'all' && category !== 'limited')
.map(Number)
.sort((a, b) => a - b),
[starGiftCategoriesByName]);
[idsByCategory]);
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
@ -80,17 +80,17 @@ const StarGiftCategoryList = ({
{renderCategoryItem('all')}
{renderCategoryItem('stock')}
{renderCategoryItem('limited')}
{starCategories.map(renderCategoryItem)}
{starCategories?.map(renderCategoryItem)}
</div>
);
};
export default memo(withGlobal(
(global): StateProps => {
const { starGiftCategoriesByName } = global;
const { starGifts } = global;
return {
starGiftCategoriesByName,
idsByCategory: starGifts?.idsByCategory,
};
},
)(StarGiftCategoryList));

View File

@ -0,0 +1,55 @@
.root {
--_height: 15rem;
display: flex;
flex-direction: column;
align-items: center;
height: var(--_height);
margin-bottom: 0.5rem;
padding-bottom: 1rem;
}
.radialPattern {
position: absolute;
inset: -5%;
top: -5rem;
height: calc(var(--height) * 1.05);
z-index: -1;
}
.sticker {
margin-top: 2rem;
}
.transition {
position: absolute;
top: 0;
height: calc(var(--_height) + 1rem); // Account for top modal padding
overflow: hidden;
}
.transitionSlide {
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 1.25rem;
color: white;
margin-top: auto;
}
.subtitle {
max-width: 75%;
font-size: 0.875rem;
transition: color 150ms ease-in;
text-align: center;
text-wrap: balance;
}
.title, .subtitle {
margin-bottom: 0;
z-index: 1;
}

View File

@ -0,0 +1,80 @@
import React, { memo, useMemo } from '../../../lib/teact/teact';
import type {
ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributePattern,
} from '../../../api/types';
import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
import { useTransitionActiveKey } from '../../../hooks/animations/useTransitionActiveKey';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
import Transition from '../../ui/Transition';
import styles from './UniqueGiftHeader.module.scss';
type OwnProps = {
modelAttribute: ApiStarGiftAttributeModel;
backdropAttribute: ApiStarGiftAttributeBackdrop;
patternAttribute: ApiStarGiftAttributePattern;
title?: string;
subtitle?: string;
className?: string;
};
const STICKER_SIZE = 120;
const UniqueGiftHeader = ({
modelAttribute,
backdropAttribute,
patternAttribute,
title,
subtitle,
className,
}: OwnProps) => {
const activeKey = useTransitionActiveKey([modelAttribute, backdropAttribute, patternAttribute]);
const subtitleColor = backdropAttribute?.textColor;
const radialPatternBackdrop = useMemo(() => {
const backdropColors = [backdropAttribute.centerColor, backdropAttribute.edgeColor];
const patternColor = backdropAttribute.patternColor;
return (
<RadialPatternBackground
className={styles.radialPattern}
backgroundColors={backdropColors}
patternColor={patternColor}
patternIcon={patternAttribute.sticker}
/>
);
}, [backdropAttribute, patternAttribute]);
return (
<div className={buildClassName(styles.root, className)}>
<Transition
className={styles.transition}
slideClassName={styles.transitionSlide}
activeKey={activeKey}
direction={1}
name="zoomBounceSemiFade"
>
{radialPatternBackdrop}
<AnimatedIconFromSticker
className={styles.sticker}
sticker={modelAttribute.sticker}
size={STICKER_SIZE}
/>
</Transition>
{title && <h1 className={styles.title}>{title}</h1>}
{subtitle && (
<p className={styles.subtitle} style={buildStyle(subtitleColor && `color: ${subtitleColor}`)}>
{subtitle}
</p>
)}
</div>
);
};
export default memo(UniqueGiftHeader);

View File

@ -32,11 +32,16 @@
color: var(--_color-description, var(--color-text));
}
.footer {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footerDescription {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-align: center;
margin-bottom: 1rem;
}
.unknown {
@ -82,3 +87,7 @@
font-size: 0.875rem;
}
}
.starAmountIcon {
margin-inline-start: 0 !important;
}

View File

@ -59,6 +59,7 @@ const GiftInfoModal = ({
convertGiftToStars,
openChatWithInfo,
focusMessage,
openGiftUpgradeModal,
} = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
@ -81,16 +82,16 @@ const GiftInfoModal = ({
const giftSticker = gift && getStickerFromGift(gift);
const canFocusUpgrade = Boolean(userGift?.upgradeMsgId);
const canUpdate = Boolean(userGift?.messageId) && !isSender && !canFocusUpgrade;
const canUpdate = Boolean(userGift?.messageId) && targetUser?.id === currentUserId && !canFocusUpgrade;
const handleClose = useLastCallback(() => {
closeGiftInfoModal();
});
const handleFocusUpgraded = useLastCallback(() => {
if (!userGift?.upgradeMsgId) return;
const { upgradeMsgId, fromId } = userGift;
focusMessage({ chatId: fromId!, messageId: upgradeMsgId! });
if (!userGift?.upgradeMsgId || !targetUser) return;
const { upgradeMsgId } = userGift;
focusMessage({ chatId: targetUser.id, messageId: upgradeMsgId! });
handleClose();
});
@ -107,9 +108,9 @@ const GiftInfoModal = ({
handleClose();
});
const handleOpenProfile = useLastCallback(() => {
openChatWithInfo({ id: currentUserId!, profileTab: 'gifts' });
handleClose();
const handleOpenUpgradeModal = useLastCallback(() => {
if (!userGift) return;
openGiftUpgradeModal({ giftId: userGift.gift.id, gift: userGift });
});
const giftAttributes = useMemo(() => {
@ -136,6 +137,30 @@ const GiftInfoModal = ({
);
}, [giftAttributes, isOpen]);
const renderFooterButton = useLastCallback(() => {
if (canFocusUpgrade) {
return (
<Button size="smaller" onClick={handleFocusUpgraded}>
{lang('GiftInfoViewUpgraded')}
</Button>
);
}
if (canUpdate && userGift?.alreadyPaidUpgradeStars && !userGift.upgradeMsgId) {
return (
<Button size="smaller" isShiny onClick={handleOpenUpgradeModal}>
{lang('GiftInfoUpgradeForFree')}
</Button>
);
}
return (
<Button size="smaller" onClick={handleClose}>
{lang('OK')}
</Button>
);
});
const modalData = useMemo(() => {
if (!typeGift || !gift) {
return undefined;
@ -154,9 +179,14 @@ const GiftInfoModal = ({
});
}
if (!userGift) return lang('GiftInfoSoldOutDescription');
if (userGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded');
if (userGift.canUpgrade && userGift.alreadyPaidUpgradeStars) {
return canUpdate
? lang('GiftInfoDescriptionFreeUpgrade')
: lang('GiftInfoDescriptionFreeUpgradeOut', { user: getUserFullName(targetUser)! });
}
if (!canUpdate && !isSender) return undefined;
if (!starsToConvert || canConvertDifference < 0) return undefined;
if (isConverted) {
if (isConverted && starsToConvert) {
return canUpdate
? lang('GiftInfoDescriptionConverted', {
amount: formatInteger(starsToConvert!),
@ -175,13 +205,23 @@ const GiftInfoModal = ({
});
}
if (userGift.canUpgrade && canUpdate) {
return lang('GiftInfoDescriptionUpgrade', {
amount: formatInteger(starsToConvert!),
}, {
pluralValue: starsToConvert!,
withNodes: true,
withMarkdown: true,
});
}
return canUpdate
? lang('GiftInfoDescription', {
amount: starsToConvert,
}, {
withNodes: true,
withMarkdown: true,
pluralValue: starsToConvert,
pluralValue: starsToConvert || 0,
})
: lang('GiftInfoDescriptionOut', {
amount: starsToConvert,
@ -189,7 +229,7 @@ const GiftInfoModal = ({
}, {
withNodes: true,
withMarkdown: true,
pluralValue: starsToConvert,
pluralValue: starsToConvert || 0,
});
})();
@ -269,10 +309,12 @@ const GiftInfoModal = ({
]);
}
const starsValue = gift.stars + (userGift?.alreadyPaidUpgradeStars || 0);
tableData.push([
lang('GiftInfoValue'),
<div className={styles.giftValue}>
{formatStarsAsIcon(lang, gift.stars)}
{formatStarsAsIcon(lang, starsValue, { className: styles.starAmountIcon })}
{canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && (
<BadgeButton onClick={openConvertConfirm}>
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
@ -296,7 +338,10 @@ const GiftInfoModal = ({
if (gift.upgradeStars) {
tableData.push([
lang('GiftInfoStatus'),
lang('GiftInfoStatusNonUnique'),
<div className={styles.giftValue}>
{lang('GiftInfoStatusNonUnique')}
{canUpdate && <BadgeButton onClick={handleOpenUpgradeModal}>{lang('GiftInfoUpgradeBadge')}</BadgeButton>}
</div>,
]);
}
@ -424,12 +469,11 @@ const GiftInfoModal = ({
{canUpdate && (
<div className={styles.footerDescription}>
<div>
{isUnsaved ? lang('GiftInfoHidden')
: lang('GiftInfoSaved', {
link: <Link isPrimary onClick={handleOpenProfile}>{lang('GiftInfoSavedView')}</Link>,
}, {
withNodes: true,
})}
{lang(isUnsaved ? 'GiftInfoHidden' : 'GiftInfoSaved', {
link: <Link isPrimary onClick={handleTriggerVisibility}>{lang('GiftInfoSavedHide')}</Link>,
}, {
withNodes: true,
})}
</div>
{isVisibleForMe && (
<div>
@ -438,21 +482,7 @@ const GiftInfoModal = ({
)}
</div>
)}
{canFocusUpgrade && (
<Button size="smaller" onClick={handleFocusUpgraded}>
{lang('GiftInfoViewUpgraded')}
</Button>
)}
{!canUpdate && !canFocusUpgrade && (
<Button size="smaller" onClick={handleClose}>
{lang('OK')}
</Button>
)}
{canUpdate && (
<Button size="smaller" onClick={handleTriggerVisibility}>
{lang(isUnsaved ? 'GiftInfoMakeVisible' : 'GiftInfoMakeInvisible')}
</Button>
)}
{renderFooterButton()}
</div>
);
@ -463,7 +493,7 @@ const GiftInfoModal = ({
};
}, [
typeGift, userGift, targetUser, giftSticker, lang, canUpdate, canConvertDifference, isSender, oldLang, gift,
radialPatternBackdrop, giftAttributes, canFocusUpgrade,
radialPatternBackdrop, giftAttributes, renderFooterButton,
]);
return (

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../../lib/teact/teact';
import React from '../../../../lib/teact/teact';
import type { OwnProps } from './GiftUpgradeModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const GiftUpgradeModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const GiftUpgradeModal = useModuleLoader(Bundles.Stars, 'GiftUpgradeModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading
return GiftUpgradeModal ? <GiftUpgradeModal {...props} /> : undefined;
};
export default GiftUpgradeModalAsync;

View File

@ -0,0 +1,11 @@
.footer {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-self: stretch;
}
.footerButton {
margin-top: 0.5rem;
}

View File

@ -0,0 +1,206 @@
import React, {
memo, useEffect, useMemo, useState,
} from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type {
ApiPeer,
ApiStarGiftAttribute,
ApiStarGiftAttributeBackdrop,
ApiStarGiftAttributeModel,
ApiStarGiftAttributePattern,
ApiStarGiftRegular,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { ApiMediaFormat } from '../../../../api/types';
import { getPeerTitle, getStickerMediaHash } from '../../../../global/helpers';
import { selectPeer } from '../../../../global/selectors';
import { formatStarsAsIcon } from '../../../../util/localization/format';
import { fetch } from '../../../../util/mediaLoader';
import useInterval from '../../../../hooks/schedulers/useInterval';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import Button from '../../../ui/Button';
import Checkbox from '../../../ui/Checkbox';
import TableAboutModal, { type TableAboutData } from '../../common/TableAboutModal';
import UniqueGiftHeader from '../UniqueGiftHeader';
import styles from './GiftUpgradeModal.module.scss';
export type OwnProps = {
modal: TabState['giftUpgradeModal'];
};
type StateProps = {
recipient?: ApiPeer;
};
type Attributes = {
model: ApiStarGiftAttributeModel;
pattern: ApiStarGiftAttributePattern;
backdrop: ApiStarGiftAttributeBackdrop;
};
const PREVIEW_UPDATE_INTERVAL = 3000;
const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
const { closeGiftUpgradeModal, upgradeGift } = getActions();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const renderingRecipient = useCurrentOrPrev(recipient);
const [shouldKeepOriginalDetails, setShouldKeepOriginalDetails] = useState(false);
const [previewAttributes, setPreviewAttributes] = useState<Attributes | undefined>();
const lang = useLang();
const handleClose = useLastCallback(() => closeGiftUpgradeModal());
const handleUpgrade = useLastCallback(() => {
const gift = renderingModal?.gift;
if (!gift?.messageId) return;
upgradeGift({
messageId: gift.messageId,
shouldKeepOriginalDetails,
upgradeStars: !gift.alreadyPaidUpgradeStars ? (gift.gift as ApiStarGiftRegular).upgradeStars : undefined,
});
handleClose();
});
const updatePreviewAttributes = useLastCallback(() => {
if (!renderingModal?.sampleAttributes) return;
setPreviewAttributes(getRandomAttributes(renderingModal.sampleAttributes, previewAttributes));
});
useInterval(updatePreviewAttributes, isOpen ? PREVIEW_UPDATE_INTERVAL : undefined, true);
useEffect(() => {
if (isOpen && renderingModal?.sampleAttributes) {
updatePreviewAttributes();
}
}, [isOpen, renderingModal?.sampleAttributes]);
// Preload stickers and patterns
useEffect(() => {
const attributes = renderingModal?.sampleAttributes;
if (!attributes) return;
const patternStickers = attributes.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'pattern')
.map((attr) => attr.sticker);
const modelStickers = attributes.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model')
.map((attr) => attr.sticker);
const mediaHashes = [...patternStickers, ...modelStickers].map((sticker) => getStickerMediaHash(sticker, 'full'));
mediaHashes.forEach((hash) => {
fetch(hash, ApiMediaFormat.BlobUrl);
});
}, [renderingModal?.sampleAttributes]);
const modalData = useMemo(() => {
if (!previewAttributes) {
return undefined;
}
const gift = renderingModal?.gift;
const listItemData = [
['diamond', lang('GiftUpgradeUniqueTitle'), lang('GiftUpgradeUniqueDescription')],
['trade', lang('GiftUpgradeTransferableTitle'), lang('GiftUpgradeTransferableDescription')],
['auction', lang('GiftUpgradeTradeableTitle'), lang('GiftUpgradeTradeableDescription')],
] satisfies TableAboutData;
const subtitle = renderingRecipient
? lang('GiftUpgradeText', { peer: getPeerTitle(lang, renderingRecipient) })
: lang('GiftUpgradeTextOwn');
const header = (
<UniqueGiftHeader
modelAttribute={previewAttributes.model}
backdropAttribute={previewAttributes.backdrop}
patternAttribute={previewAttributes.pattern}
title={lang('GiftUpgradeTitle')}
subtitle={subtitle}
/>
);
const footer = (
<div className={styles.footer}>
{!gift && (
<Button className={styles.footerButton} size="smaller" onClick={handleClose}>
{lang('OK')}
</Button>
)}
{gift && (
<>
<Checkbox
label={lang('GiftUpgradeKeepDetails')}
onCheck={setShouldKeepOriginalDetails}
checked={shouldKeepOriginalDetails}
/>
<Button className={styles.footerButton} size="smaller" isShiny onClick={handleUpgrade}>
{gift.alreadyPaidUpgradeStars
? lang('GeneralConfirm')
: lang('GiftUpgradeButton', {
amount: formatStarsAsIcon(lang, (gift.gift as ApiStarGiftRegular).upgradeStars!, { asFont: true }),
}, { withNodes: true })}
</Button>
</>
)}
</div>
);
return {
listItemData,
header,
footer,
};
}, [previewAttributes, lang, renderingRecipient, renderingModal?.gift, shouldKeepOriginalDetails]);
return (
<TableAboutModal
isOpen={isOpen}
header={modalData?.header}
footer={modalData?.footer}
listItemData={modalData?.listItemData}
hasBackdrop
onClose={handleClose}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const recipientId = modal?.recipientId;
const recipient = recipientId ? selectPeer(global, recipientId) : undefined;
return {
recipient,
};
},
)(GiftUpgradeModal));
function getRandomAttributes(list: ApiStarGiftAttribute[], previousSelection?: Attributes): Attributes {
const models = list.filter((attr): attr is ApiStarGiftAttributeModel => (
attr.type === 'model' && attr.name !== previousSelection?.model.name
));
const patterns = list.filter((attr): attr is ApiStarGiftAttributePattern => (
attr.type === 'pattern' && attr.name !== previousSelection?.pattern.name
));
const backdrops = list.filter((attr): attr is ApiStarGiftAttributeBackdrop => (
attr.type === 'backdrop' && attr.name !== previousSelection?.backdrop.name
));
const randomModel = models[Math.floor(Math.random() * models.length)];
const randomPattern = patterns[Math.floor(Math.random() * patterns.length)];
const randomBackdrop = backdrops[Math.floor(Math.random() * backdrops.length)];
return {
model: randomModel,
pattern: randomPattern,
backdrop: randomBackdrop,
};
}

View File

@ -200,7 +200,7 @@ const StarPaymentModal = ({
</div>
<Button className={styles.paymentButton} size="smaller" onClick={handlePayment} isLoading={isLoading}>
{lang(isBotSubscription ? 'StarsSubscribeBotButtonMonth' : 'StarsPay', {
amount: formatStarsAsIcon(lang, amount!, true),
amount: formatStarsAsIcon(lang, amount!, { asFont: true }),
}, {
withNodes: true,
})}

View File

@ -86,7 +86,7 @@
}
> .Switcher {
margin-left: auto;
margin-inline-start: auto;
}
}

View File

@ -255,6 +255,17 @@
}
}
&-zoomBounceSemiFade,
&-zoomBounceSemiFadeBackwards {
> .Transition_slide-from {
animation: zoom-bounce 0.25s ease-in-out;
}
> .Transition_slide-to {
animation: fade-in-opacity 250ms ease, zoom-bounce 0.25s ease-in-out;
}
}
&-fade,
&-fadeBackwards {
> .Transition_slide-from {
@ -741,3 +752,17 @@
clip-path: inset(0 100% 0 0);
}
}
@keyframes zoom-bounce {
0% {
transform: scale(1);
}
25% {
transform: scale(0.95);
}
100% {
transform: scale(1);
}
}

View File

@ -21,7 +21,7 @@ import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import './Transition.scss';
type AnimationName = (
'none' | 'slide' | 'slideRtl' | 'slideFade' | 'zoomFade' | 'slideLayers'
'none' | 'slide' | 'slideRtl' | 'slideFade' | 'zoomFade' | 'zoomBounceSemiFade' | 'slideLayers'
| 'fade' | 'pushSlide' | 'reveal' | 'slideOptimized' | 'slideOptimizedRtl' | 'semiFade'
| 'slideVertical' | 'slideVerticalFade' | 'slideFadeAndroid'
);
@ -63,7 +63,7 @@ export const ACTIVE_SLIDE_CLASS_NAME = CLASSES.active;
export const TO_SLIDE_CLASS_NAME = CLASSES.to;
const DISABLEABLE_ANIMATIONS = new Set<AnimationName>([
'slide', 'slideRtl', 'slideFade', 'zoomFade', 'slideLayers', 'pushSlide', 'reveal',
'slide', 'slideRtl', 'slideFade', 'zoomFade', 'zoomBounceSemiFade', 'slideLayers', 'pushSlide', 'reveal',
'slideOptimized', 'slideOptimizedRtl', 'slideVertical', 'slideVerticalFade',
]);

View File

@ -1,4 +1,6 @@
import type { ApiInputInvoiceStarGift, ApiRequestInputInvoice } from '../../../api/types';
import type {
ApiInputInvoice, ApiInputInvoiceStarGift, ApiInputInvoiceStarGiftUpgrade, ApiRequestInputInvoice,
} from '../../../api/types';
import type { ApiCredentials } from '../../../components/payment/PaymentModal';
import type {
ActionReturnType, GlobalState, TabArgs,
@ -16,7 +18,9 @@ import { isChatChannel, isChatSuperGroup } from '../../helpers';
import {
getRequestInputInvoice,
} from '../../helpers/payments';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
addActionHandler, getActions, getGlobal, setGlobal,
} from '../../index';
import {
closeInvoice,
openStarsTransactionFromReceipt,
@ -117,62 +121,21 @@ addActionHandler('openInvoice', async (global, actions, payload): Promise<void>
setGlobal(global);
});
addActionHandler('sendStarGift', async (global, actions, payload): Promise<void> => {
addActionHandler('sendStarGift', (global, actions, payload): ActionReturnType => {
const {
gift, userId, message, shouldHideName, tabId = getCurrentTabId(),
gift, userId, message, shouldHideName, shouldUpgrade, tabId = getCurrentTabId(),
} = payload;
const balance = global.stars?.balance;
if (balance === undefined) return;
if (balance.amount < gift.stars) {
actions.openStarsBalanceModal({ tabId });
return;
}
const inputInvoice: ApiInputInvoiceStarGift = {
type: 'stargift',
userId,
giftId: gift.id,
message,
shouldHideName,
shouldUpgrade: shouldUpgrade || undefined,
};
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
if (!requestInputInvoice) {
return;
}
global = updateTabState(global, {
isPaymentFormLoading: true,
}, tabId);
setGlobal(global);
const theme = extractCurrentThemeParams();
const form = await callApi('getPaymentForm', requestInputInvoice, theme);
if (!form) {
return;
}
global = getGlobal();
global = updateTabState(global, {
isPaymentFormLoading: false,
}, tabId);
setGlobal(global);
if ('error' in form) {
return;
}
actions.sendStarPaymentForm({
starGift: {
inputInvoice,
formId: form.formId,
},
tabId,
});
payInputStarInvoice(global, inputInvoice, gift.stars, tabId);
});
addActionHandler('getReceipt', async (global, actions, payload): Promise<void> => {
@ -296,9 +259,9 @@ addActionHandler('sendPaymentForm', async (global, actions, payload): Promise<vo
});
addActionHandler('sendStarPaymentForm', async (global, actions, payload): Promise<void> => {
const { starGift, tabId = getCurrentTabId() } = payload;
const { directInfo, tabId = getCurrentTabId() } = payload;
const starPayment = selectStarsPayment(global, tabId);
const inputInvoice = starPayment?.inputInvoice || starGift?.inputInvoice;
const inputInvoice = starPayment?.inputInvoice || directInfo?.inputInvoice;
if (!inputInvoice) return;
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
@ -306,7 +269,7 @@ addActionHandler('sendStarPaymentForm', async (global, actions, payload): Promis
return;
}
const formId = (starPayment.form?.formId || starPayment.subscriptionInfo?.subscriptionFormId || starGift?.formId)!;
const formId = (starPayment.form?.formId || starPayment.subscriptionInfo?.subscriptionFormId || directInfo?.formId)!;
global = updateStarsPayment(global, { status: 'pending' }, tabId);
setGlobal(global);
@ -337,7 +300,7 @@ addActionHandler('sendStarPaymentForm', async (global, actions, payload): Promis
actions.apiUpdate({
'@type': 'updateStarPaymentStateCompleted',
paymentState: starGift ? { inputInvoice } : starPayment,
paymentState: directInfo ? { inputInvoice } : starPayment,
tabId,
});
actions.loadStarStatus();
@ -991,6 +954,90 @@ addActionHandler('launchPrepaidStarsGiveaway', async (global, actions, payload):
actions.openBoostStatistics({ chatId, tabId });
});
addActionHandler('upgradeGift', (global, actions, payload): ActionReturnType => {
const {
messageId, shouldKeepOriginalDetails, upgradeStars, tabId = getCurrentTabId(),
} = payload;
global = updateTabState(global, {
isWaitingForStarGiftUpgrade: true,
}, tabId);
setGlobal(global);
global = getGlobal();
actions.closeGiftUpgradeModal({ tabId });
actions.closeGiftInfoModal({ tabId });
if (!upgradeStars) {
callApi('upgradeGift', {
messageId,
shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
});
return;
}
const invoice: ApiInputInvoiceStarGiftUpgrade = {
type: 'stargiftUpgrade',
messageId,
shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
};
payInputStarInvoice(global, invoice, upgradeStars, tabId);
});
async function payInputStarInvoice<T extends GlobalState>(
global: T, inputInvoice: ApiInputInvoice, price: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const actions = getActions();
const balance = global.stars?.balance;
if (balance === undefined) return;
if (balance.amount < price) {
actions.openStarsBalanceModal({ tabId });
return;
}
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
if (!requestInputInvoice) {
return;
}
global = updateTabState(global, {
isPaymentFormLoading: true,
}, tabId);
setGlobal(global);
const theme = extractCurrentThemeParams();
const form = await callApi('getPaymentForm', requestInputInvoice, theme);
if (!form) {
return;
}
global = getGlobal();
global = updateTabState(global, {
isPaymentFormLoading: false,
}, tabId);
setGlobal(global);
if ('error' in form) {
return;
}
actions.sendStarPaymentForm({
directInfo: {
inputInvoice,
formId: form.formId,
},
tabId,
});
}
addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promise<void> => {
const {
slug, tabId = getCurrentTabId(),

View File

@ -12,6 +12,7 @@ import {
updateStarsBalance,
updateStarsSubscriptionLoading,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
selectPeer,
selectUser,
@ -91,16 +92,16 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
return;
}
const starGiftsById = buildCollectionByKey(result, 'id');
const byId = buildCollectionByKey(result, 'id');
const starGiftCategoriesByName: Record<StarGiftCategory, string[]> = {
const idsByCategoryName: Record<StarGiftCategory, string[]> = {
all: [],
stock: [],
limited: [],
};
const allStarGiftIds = Object.keys(starGiftsById);
const allStarGifts = Object.values(starGiftsById);
const allStarGiftIds = Object.keys(byId);
const allStarGifts = Object.values(byId);
const limitedStarGiftIds = allStarGifts.map((gift) => (gift.isLimited ? gift.id : undefined))
.filter(Boolean) as string[];
@ -109,23 +110,25 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
gift.availabilityRemains || !gift.availabilityTotal ? gift.id : undefined
)).filter(Boolean) as string[];
starGiftCategoriesByName.all = allStarGiftIds;
starGiftCategoriesByName.limited = limitedStarGiftIds;
starGiftCategoriesByName.stock = stockedStarGiftIds;
idsByCategoryName.all = allStarGiftIds;
idsByCategoryName.limited = limitedStarGiftIds;
idsByCategoryName.stock = stockedStarGiftIds;
allStarGifts.forEach((gift) => {
const starsCategory = gift.stars;
if (!starGiftCategoriesByName[starsCategory]) {
starGiftCategoriesByName[starsCategory] = [];
if (!idsByCategoryName[starsCategory]) {
idsByCategoryName[starsCategory] = [];
}
starGiftCategoriesByName[starsCategory].push(gift.id);
idsByCategoryName[starsCategory].push(gift.id);
});
global = getGlobal();
global = {
...global,
starGiftsById,
starGiftCategoriesByName,
starGifts: {
byId,
idsByCategory: idsByCategoryName,
},
};
setGlobal(global);
});
@ -218,17 +221,19 @@ addActionHandler('changeGiftVisibility', async (global, actions, payload): Promi
const currentUserId = global.currentUserId!;
const oldGifts = global.users.giftsById[currentUserId];
const newGifts = oldGifts.gifts.map((gift) => {
if (gift.messageId === messageId) {
return {
...gift,
isUnsaved: shouldUnsave,
} satisfies ApiUserStarGift;
}
return gift;
});
global = replaceUserGifts(global, currentUserId, newGifts, oldGifts.nextOffset);
setGlobal(global);
if (oldGifts?.gifts?.length) {
const newGifts = oldGifts.gifts.map((gift) => {
if (gift.messageId === messageId) {
return {
...gift,
isUnsaved: shouldUnsave,
} satisfies ApiUserStarGift;
}
return gift;
});
global = replaceUserGifts(global, currentUserId, newGifts, oldGifts.nextOffset);
setGlobal(global);
}
const result = await callApi('saveStarGift', {
messageId,
@ -260,3 +265,27 @@ addActionHandler('convertGiftToStars', async (global, actions, payload): Promise
actions.loadUserGifts({ userId: global.currentUserId!, shouldRefresh: true });
actions.openStarsBalanceModal({ tabId });
});
addActionHandler('openGiftUpgradeModal', async (global, actions, payload): Promise<void> => {
const {
giftId, gift, peerId, tabId = getCurrentTabId(),
} = payload;
const samples = await callApi('fetchStarGiftUpgradePreview', {
giftId,
});
if (!samples) return;
global = getGlobal();
global = updateTabState(global, {
giftUpgradeModal: {
recipientId: peerId,
gift,
sampleAttributes: samples,
},
}, tabId);
setGlobal(global);
});

View File

@ -19,7 +19,8 @@ import {
updateStealthMode,
updateThreadInfos,
} from '../../reducers';
import { selectPeerStories, selectPeerStory } from '../../selectors';
import { updateTabState } from '../../reducers/tabs';
import { selectPeerStories, selectPeerStory, selectTabState } from '../../selectors';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
@ -205,6 +206,35 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateLangPack': {
applyLangPackDifference(update.version, update.strings, update.keysToRemove);
break;
}
case 'newMessage': {
const actionStarGift = update.message.content?.action?.starGift;
if (actionStarGift?.type !== 'starGiftUnique' || !update.message.isOutgoing) {
return undefined;
}
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const tabState = selectTabState(global, tabId);
if (tabState.isWaitingForStarGiftUpgrade) {
actions.openUniqueGiftBySlug({
slug: actionStarGift.gift.slug,
tabId,
});
actions.showNotification({
title: { key: 'GiftUpgradedTitle' },
message: { key: 'GiftUpgradedDescription' },
tabId,
});
actions.requestConfetti({ withStars: true, tabId });
global = updateTabState(global, {
isWaitingForStarGiftUpgrade: undefined,
}, tabId);
}
});
}
}

View File

@ -264,6 +264,8 @@ addActionHandler('openGiftInfoModalFromMessage', (global, actions, payload): Act
messageId: (!message.isOutgoing || chatId === global.currentUserId) ? message.id : undefined,
isConverted: starGift.isConverted,
upgradeMsgId: starGift.upgradeMsgId,
canUpgrade: starGift.canUpgrade,
alreadyPaidUpgradeStars: starGift.alreadyPaidUpgradeStars,
} satisfies ApiUserStarGift;
actions.openGiftInfoModal({ userId: giftReceiverId, gift, tabId });
@ -291,3 +293,11 @@ addActionHandler('closeGiftInfoModal', (global, actions, payload): ActionReturnT
giftInfoModal: undefined,
}, tabId);
});
addActionHandler('closeGiftUpgradeModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftUpgradeModal: undefined,
}, tabId);
});

View File

@ -20,7 +20,7 @@ export function getRequestInputInvoice<T extends GlobalState>(
if (inputInvoice.type === 'stargift') {
const {
userId, shouldHideName, giftId, message,
userId, shouldHideName, giftId, message, shouldUpgrade,
} = inputInvoice;
const user = selectUser(global, userId);
@ -32,6 +32,7 @@ export function getRequestInputInvoice<T extends GlobalState>(
shouldHideName,
giftId,
message,
shouldUpgrade,
};
}
@ -172,6 +173,15 @@ export function getRequestInputInvoice<T extends GlobalState>(
};
}
if (inputInvoice.type === 'stargiftUpgrade') {
const { messageId, shouldKeepOriginalDetails } = inputInvoice;
return {
type: 'stargiftUpgrade',
messageId,
shouldKeepOriginalDetails,
};
}
return undefined;
}

View File

@ -181,12 +181,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
hash: {},
},
availableEffectById: {},
starGiftsById: {},
starGiftCategoriesByName: {
all: [],
limited: [],
stock: [],
},
stickers: {
setsById: {},

View File

@ -681,9 +681,9 @@ export interface ActionPayloads {
tipAmount?: number;
} & WithTabId;
sendStarPaymentForm: {
starGift?: {
directInfo?: {
formId: string;
inputInvoice: ApiInputInvoiceStarGift;
inputInvoice: ApiInputInvoice;
};
} & WithTabId;
getReceipt: {
@ -2311,6 +2311,17 @@ export interface ActionPayloads {
gift: ApiStarGift;
}) & WithTabId;
closeGiftInfoModal: WithTabId | undefined;
openGiftUpgradeModal: {
giftId: string;
peerId?: string;
gift?: ApiUserStarGift;
} & WithTabId;
closeGiftUpgradeModal: WithTabId | undefined;
upgradeGift: {
messageId: number;
shouldKeepOriginalDetails?: boolean;
upgradeStars?: number;
} & WithTabId;
loadUserGifts: {
userId: string;
shouldRefresh?: boolean;

View File

@ -298,8 +298,10 @@ export type GlobalState = {
};
};
availableEffectById: Record<string, ApiAvailableEffect>;
starGiftsById: Record<string, ApiStarGiftRegular>;
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
starGifts?: {
byId: Record<string, ApiStarGiftRegular>;
idsByCategory: Record<StarGiftCategory, string[]>;
};
stickers: {
setsById: Record<string, ApiStickerSet>;

View File

@ -33,6 +33,7 @@ import type {
ApiReactionWithPaid,
ApiReceiptRegular,
ApiStarGift,
ApiStarGiftAttribute,
ApiStarGiveawayOption,
ApiStarsSubscription,
ApiStarsTransaction,
@ -713,10 +714,18 @@ export type TabState = {
gift: ApiUserStarGift | ApiStarGift;
};
giftUpgradeModal?: {
sampleAttributes: ApiStarGiftAttribute[];
recipientId?: string;
gift?: ApiUserStarGift;
};
suggestedStatusModal?: {
botId: string;
webAppKey?: string;
customEmojiId: string;
duration?: number;
};
isWaitingForStarGiftUpgrade?: true;
};

View File

@ -0,0 +1,17 @@
import { useMemo, useRef } from '../../lib/teact/teact';
/**
* Use this hook to bind `<Transition />` animation to changes in the dependency array.
* Use optional parameter `noAnimation` if you want to prevent the animation even if the dependency array changes.
*/
export function useTransitionActiveKey(deps: unknown[], noAnimation?: boolean): number {
const activeKey = useRef(0);
let didUpdate = false;
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
useMemo(() => { activeKey.current += 1; didUpdate = true; }, deps);
if (noAnimation && didUpdate) activeKey.current -= 1;
return activeKey.current;
}

View File

@ -20292,6 +20292,11 @@ namespace Api {
}>, Bool> {
msgId: int;
}
export class GetUniqueStarGift extends Request<Partial<{
slug: string;
}>, payments.TypeUniqueStarGift> {
slug: string;
}
export class BotCancelStarsSubscription extends Request<Partial<{
// flags: Api.Type;
restore?: true;
@ -21346,7 +21351,7 @@ namespace Api {
| help.GetConfig | help.GetNearestDc | help.GetAppUpdate | help.GetInviteText | help.GetSupport | help.SetBotUpdatesStatus | help.GetCdnConfig | help.GetRecentMeUrls | help.GetTermsOfServiceUpdate | help.AcceptTermsOfService | help.GetDeepLinkInfo | help.GetAppConfig | help.SaveAppLog | help.GetPassportConfig | help.GetSupportName | help.GetUserInfo | help.EditUserInfo | help.GetPromoData | help.HidePromoData | help.DismissSuggestion | help.GetCountriesList | help.GetPremiumPromo | help.GetPeerColors | help.GetPeerProfileColors | help.GetTimezonesList
| channels.ReadHistory | channels.DeleteMessages | channels.ReportSpam | channels.GetMessages | channels.GetParticipants | channels.GetParticipant | channels.GetChannels | channels.GetFullChannel | channels.CreateChannel | channels.EditAdmin | channels.EditTitle | channels.EditPhoto | channels.CheckUsername | channels.UpdateUsername | channels.JoinChannel | channels.LeaveChannel | channels.InviteToChannel | channels.DeleteChannel | channels.ExportMessageLink | channels.ToggleSignatures | channels.GetAdminedPublicChannels | channels.EditBanned | channels.GetAdminLog | channels.SetStickers | channels.ReadMessageContents | channels.DeleteHistory | channels.TogglePreHistoryHidden | channels.GetLeftChannels | channels.GetGroupsForDiscussion | channels.SetDiscussionGroup | channels.EditCreator | channels.EditLocation | channels.ToggleSlowMode | channels.GetInactiveChannels | channels.ConvertToGigagroup | channels.GetSendAs | channels.DeleteParticipantHistory | channels.ToggleJoinToSend | channels.ToggleJoinRequest | channels.ReorderUsernames | channels.ToggleUsername | channels.DeactivateAllUsernames | channels.ToggleForum | channels.CreateForumTopic | channels.GetForumTopics | channels.GetForumTopicsByID | channels.EditForumTopic | channels.UpdatePinnedForumTopic | channels.DeleteTopicHistory | channels.ReorderPinnedForumTopics | channels.ToggleAntiSpam | channels.ReportAntiSpamFalsePositive | channels.ToggleParticipantsHidden | channels.UpdateColor | channels.ToggleViewForumAsMessages | channels.GetChannelRecommendations | channels.UpdateEmojiStatus | channels.SetBoostsToUnblockRestrictions | channels.SetEmojiStickers | channels.RestrictSponsoredMessages | channels.SearchPosts
| bots.SendCustomRequest | bots.AnswerWebhookJSONQuery | bots.SetBotCommands | bots.ResetBotCommands | bots.GetBotCommands | bots.SetBotMenuButton | bots.GetBotMenuButton | bots.SetBotBroadcastDefaultAdminRights | bots.SetBotGroupDefaultAdminRights | bots.SetBotInfo | bots.GetBotInfo | bots.ReorderUsernames | bots.ToggleUsername | bots.CanSendMessage | bots.AllowSendMessage | bots.InvokeWebViewCustomMethod | bots.GetPopularAppBots | bots.AddPreviewMedia | bots.EditPreviewMedia | bots.DeletePreviewMedia | bots.ReorderPreviewMedias | bots.GetPreviewInfo | bots.GetPreviewMedias | bots.UpdateUserEmojiStatus | bots.ToggleUserEmojiStatusPermission | bots.CheckDownloadFileParams | bots.GetAdminedBots | bots.UpdateStarRefProgram | bots.SetCustomVerification | bots.GetBotRecommendations
| payments.GetPaymentForm | payments.GetPaymentReceipt | payments.ValidateRequestedInfo | payments.SendPaymentForm | payments.GetSavedInfo | payments.ClearSavedInfo | payments.GetBankCardData | payments.ExportInvoice | payments.AssignAppStoreTransaction | payments.AssignPlayMarketTransaction | payments.CanPurchasePremium | payments.GetPremiumGiftCodeOptions | payments.CheckGiftCode | payments.ApplyGiftCode | payments.GetGiveawayInfo | payments.LaunchPrepaidGiveaway | payments.GetStarsTopupOptions | payments.GetStarsStatus | payments.GetStarsTransactions | payments.SendStarsForm | payments.RefundStarsCharge | payments.GetStarsRevenueStats | payments.GetStarsRevenueWithdrawalUrl | payments.GetStarsRevenueAdsAccountUrl | payments.GetStarsTransactionsByID | payments.GetStarsGiftOptions | payments.GetStarsSubscriptions | payments.ChangeStarsSubscription | payments.FulfillStarsSubscription | payments.GetStarsGiveawayOptions | payments.GetStarGifts | payments.GetUserStarGifts | payments.SaveStarGift | payments.ConvertStarGift | payments.BotCancelStarsSubscription | payments.GetConnectedStarRefBots | payments.GetConnectedStarRefBot | payments.GetSuggestedStarRefBots | payments.ConnectStarRefBot | payments.EditConnectedStarRefBot | payments.GetStarGiftUpgradePreview | payments.UpgradeStarGift | payments.TransferStarGift | payments.GetUserStarGift | payments.GetUniqueStarGift
| payments.GetPaymentForm | payments.GetPaymentReceipt | payments.ValidateRequestedInfo | payments.SendPaymentForm | payments.GetSavedInfo | payments.ClearSavedInfo | payments.GetBankCardData | payments.ExportInvoice | payments.AssignAppStoreTransaction | payments.AssignPlayMarketTransaction | payments.CanPurchasePremium | payments.GetPremiumGiftCodeOptions | payments.CheckGiftCode | payments.ApplyGiftCode | payments.GetGiveawayInfo | payments.LaunchPrepaidGiveaway | payments.GetStarsTopupOptions | payments.GetStarsStatus | payments.GetStarsTransactions | payments.SendStarsForm | payments.RefundStarsCharge | payments.GetStarsRevenueStats | payments.GetStarsRevenueWithdrawalUrl | payments.GetStarsRevenueAdsAccountUrl | payments.GetStarsTransactionsByID | payments.GetStarsGiftOptions | payments.GetStarsSubscriptions | payments.ChangeStarsSubscription | payments.FulfillStarsSubscription | payments.GetStarsGiveawayOptions | payments.GetStarGifts | payments.GetUserStarGifts | payments.SaveStarGift | payments.ConvertStarGift | payments.GetUniqueStarGift | payments.BotCancelStarsSubscription | payments.GetConnectedStarRefBots | payments.GetConnectedStarRefBot | payments.GetSuggestedStarRefBots | payments.ConnectStarRefBot | payments.EditConnectedStarRefBot | payments.GetStarGiftUpgradePreview | payments.UpgradeStarGift | payments.TransferStarGift | payments.GetUserStarGift | payments.GetUniqueStarGift
| stickers.CreateStickerSet | stickers.RemoveStickerFromSet | stickers.ChangeStickerPosition | stickers.AddStickerToSet | stickers.SetStickerSetThumb | stickers.CheckShortName | stickers.SuggestShortName | stickers.ChangeSticker | stickers.RenameStickerSet | stickers.DeleteStickerSet | stickers.ReplaceSticker
| phone.GetCallConfig | phone.RequestCall | phone.AcceptCall | phone.ConfirmCall | phone.ReceivedCall | phone.DiscardCall | phone.SetCallRating | phone.SaveCallDebug | phone.SendSignalingData | phone.CreateGroupCall | phone.JoinGroupCall | phone.LeaveGroupCall | phone.InviteToGroupCall | phone.DiscardGroupCall | phone.ToggleGroupCallSettings | phone.GetGroupCall | phone.GetGroupParticipants | phone.CheckGroupCall | phone.ToggleGroupCallRecord | phone.EditGroupCallParticipant | phone.EditGroupCallTitle | phone.GetGroupCallJoinAs | phone.ExportGroupCallInvite | phone.ToggleGroupCallStartSubscription | phone.StartScheduledGroupCall | phone.SaveDefaultGroupCallJoinAs | phone.JoinGroupCallPresentation | phone.LeaveGroupCallPresentation | phone.GetGroupCallStreamChannels | phone.GetGroupCallStreamRtmpUrl | phone.SaveCallLog | phone.CreateConferenceCall
| langpack.GetLangPack | langpack.GetStrings | langpack.GetDifference | langpack.GetLanguages | langpack.GetLanguage

View File

@ -1715,6 +1715,10 @@ payments.getUserStarGifts#5e72c7e1 user_id:InputUser offset:string limit:int = p
payments.saveStarGift#92fd2aae flags:# unsave:flags.0?true msg_id:int = Bool;
payments.convertStarGift#72770c83 msg_id:int = Bool;
payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift;
payments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgradePreview;
payments.upgradeStarGift#cf4f0781 flags:# keep_original_details:flags.0?true msg_id:int = Updates;
payments.transferStarGift#333fb526 msg_id:int to_id:InputUser = Updates;
payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift;
phone.requestCall#a6c4600c flags:# video:flags.0?true user_id:InputUser conference_call:flags.1?InputGroupCall random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;

View File

@ -285,6 +285,31 @@
"payments.validateRequestedInfo",
"payments.sendPaymentForm",
"payments.getSavedInfo",
"payments.checkGiftCode",
"payments.applyGiftCode",
"payments.getGiveawayInfo",
"payments.getPremiumGiftCodeOptions",
"payments.launchPrepaidGiveaway",
"payments.getStarsTopupOptions",
"payments.getStarsStatus",
"payments.getStarsTransactions",
"payments.getStarsTransactionsByID",
"payments.getStarsSubscriptions",
"payments.changeStarsSubscription",
"payments.fulfillStarsSubscription",
"payments.sendStarsForm",
"payments.getStarsGiftOptions",
"payments.getStarsGiveawayOptions",
"payments.refundStarsCharge",
"payments.getStarsGiftOptions",
"payments.getStarGifts",
"payments.getUserStarGifts",
"payments.saveStarGift",
"payments.convertStarGift",
"payments.getStarGiftUpgradePreview",
"payments.upgradeStarGift",
"payments.transferStarGift",
"payments.getUniqueStarGift",
"langpack.getLangPack",
"langpack.getStrings",
"langpack.getLanguages",
@ -364,27 +389,5 @@
"premium.applyBoost",
"premium.getMyBoosts",
"premium.getBoostsList",
"payments.checkGiftCode",
"payments.applyGiftCode",
"payments.getGiveawayInfo",
"payments.getPremiumGiftCodeOptions",
"payments.launchPrepaidGiveaway",
"payments.getStarsTopupOptions",
"payments.getStarsStatus",
"payments.getStarsTransactions",
"payments.getStarsTransactionsByID",
"payments.getStarsSubscriptions",
"payments.changeStarsSubscription",
"payments.fulfillStarsSubscription",
"payments.sendStarsForm",
"payments.getStarsGiftOptions",
"payments.getStarsGiveawayOptions",
"payments.refundStarsCharge",
"payments.getStarsGiftOptions",
"payments.getStarGifts",
"payments.getUserStarGifts",
"payments.saveStarGift",
"payments.convertStarGift",
"payments.getUniqueStarGift",
"fragment.getCollectibleInfo"
]

View File

@ -48,232 +48,235 @@ $icons-map: (
"arrow-right": "\f111",
"ask-support": "\f112",
"attach": "\f113",
"author-hidden": "\f114",
"avatar-archived-chats": "\f115",
"avatar-deleted-account": "\f116",
"avatar-saved-messages": "\f117",
"bold": "\f118",
"boost-outline": "\f119",
"boost": "\f11a",
"boostcircle": "\f11b",
"boosts": "\f11c",
"bot-command": "\f11d",
"bot-commands-filled": "\f11e",
"bots": "\f11f",
"bug": "\f120",
"calendar-filter": "\f121",
"calendar": "\f122",
"camera-add": "\f123",
"camera": "\f124",
"car": "\f125",
"card": "\f126",
"cash-circle": "\f127",
"channel-filled": "\f128",
"channel": "\f129",
"channelviews": "\f12a",
"chat-badge": "\f12b",
"chats-badge": "\f12c",
"check": "\f12d",
"clock-edit": "\f12e",
"clock": "\f12f",
"close-circle": "\f130",
"close-topic": "\f131",
"close": "\f132",
"cloud-download": "\f133",
"collapse-modal": "\f134",
"collapse": "\f135",
"colorize": "\f136",
"comments-sticker": "\f137",
"comments": "\f138",
"copy-media": "\f139",
"copy": "\f13a",
"darkmode": "\f13b",
"data": "\f13c",
"delete-filled": "\f13d",
"delete-left": "\f13e",
"delete-user": "\f13f",
"delete": "\f140",
"document": "\f141",
"double-badge": "\f142",
"down": "\f143",
"download": "\f144",
"eats": "\f145",
"edit": "\f146",
"email": "\f147",
"enter": "\f148",
"expand-modal": "\f149",
"expand": "\f14a",
"eye-closed-outline": "\f14b",
"eye-closed": "\f14c",
"eye-outline": "\f14d",
"eye": "\f14e",
"favorite-filled": "\f14f",
"favorite": "\f150",
"file-badge": "\f151",
"flag": "\f152",
"folder-badge": "\f153",
"folder": "\f154",
"fontsize": "\f155",
"forums": "\f156",
"forward": "\f157",
"fullscreen": "\f158",
"gifs": "\f159",
"gift": "\f15a",
"group-filled": "\f15b",
"group": "\f15c",
"grouped-disable": "\f15d",
"grouped": "\f15e",
"hand-stop": "\f15f",
"hashtag": "\f160",
"heart-outline": "\f161",
"heart": "\f162",
"help": "\f163",
"info-filled": "\f164",
"info": "\f165",
"install": "\f166",
"italic": "\f167",
"key": "\f168",
"keyboard": "\f169",
"lamp": "\f16a",
"language": "\f16b",
"large-pause": "\f16c",
"large-play": "\f16d",
"link-badge": "\f16e",
"link-broken": "\f16f",
"link": "\f170",
"location": "\f171",
"lock-badge": "\f172",
"lock": "\f173",
"logout": "\f174",
"loop": "\f175",
"mention": "\f176",
"message-failed": "\f177",
"message-pending": "\f178",
"message-read": "\f179",
"message-succeeded": "\f17a",
"message": "\f17b",
"microphone-alt": "\f17c",
"microphone": "\f17d",
"monospace": "\f17e",
"more-circle": "\f17f",
"more": "\f180",
"move-caption-down": "\f181",
"move-caption-up": "\f182",
"mute": "\f183",
"muted": "\f184",
"my-notes": "\f185",
"new-chat-filled": "\f186",
"next": "\f187",
"nochannel": "\f188",
"noise-suppression": "\f189",
"non-contacts": "\f18a",
"one-filled": "\f18b",
"open-in-new-tab": "\f18c",
"password-off": "\f18d",
"pause": "\f18e",
"permissions": "\f18f",
"phone-discard-outline": "\f190",
"phone-discard": "\f191",
"phone": "\f192",
"photo": "\f193",
"pin-badge": "\f194",
"pin-list": "\f195",
"pin": "\f196",
"pinned-chat": "\f197",
"pinned-message": "\f198",
"pip": "\f199",
"play-story": "\f19a",
"play": "\f19b",
"poll": "\f19c",
"previous": "\f19d",
"privacy-policy": "\f19e",
"quote-text": "\f19f",
"quote": "\f1a0",
"readchats": "\f1a1",
"recent": "\f1a2",
"reload": "\f1a3",
"remove-quote": "\f1a4",
"remove": "\f1a5",
"reopen-topic": "\f1a6",
"replace": "\f1a7",
"replies": "\f1a8",
"reply-filled": "\f1a9",
"reply": "\f1aa",
"revenue-split": "\f1ab",
"revote": "\f1ac",
"save-story": "\f1ad",
"saved-messages": "\f1ae",
"schedule": "\f1af",
"search": "\f1b0",
"select": "\f1b1",
"send-outline": "\f1b2",
"send": "\f1b3",
"settings-filled": "\f1b4",
"settings": "\f1b5",
"share-filled": "\f1b6",
"share-screen-outlined": "\f1b7",
"share-screen-stop": "\f1b8",
"share-screen": "\f1b9",
"show-message": "\f1ba",
"sidebar": "\f1bb",
"skip-next": "\f1bc",
"skip-previous": "\f1bd",
"smallscreen": "\f1be",
"smile": "\f1bf",
"sort": "\f1c0",
"speaker-muted-story": "\f1c1",
"speaker-outline": "\f1c2",
"speaker-story": "\f1c3",
"speaker": "\f1c4",
"spoiler-disable": "\f1c5",
"spoiler": "\f1c6",
"sport": "\f1c7",
"star": "\f1c8",
"stars-lock": "\f1c9",
"stats": "\f1ca",
"stealth-future": "\f1cb",
"stealth-past": "\f1cc",
"stickers": "\f1cd",
"stop-raising-hand": "\f1ce",
"stop": "\f1cf",
"story-caption": "\f1d0",
"story-expired": "\f1d1",
"story-priority": "\f1d2",
"story-reply": "\f1d3",
"strikethrough": "\f1d4",
"tag-add": "\f1d5",
"tag-crossed": "\f1d6",
"tag-filter": "\f1d7",
"tag-name": "\f1d8",
"tag": "\f1d9",
"timer": "\f1da",
"toncoin": "\f1db",
"transcribe": "\f1dc",
"truck": "\f1dd",
"unarchive": "\f1de",
"underlined": "\f1df",
"unlock-badge": "\f1e0",
"unlock": "\f1e1",
"unmute": "\f1e2",
"unpin": "\f1e3",
"unread": "\f1e4",
"up": "\f1e5",
"user-filled": "\f1e6",
"user-online": "\f1e7",
"user": "\f1e8",
"video-outlined": "\f1e9",
"video-stop": "\f1ea",
"video": "\f1eb",
"view-once": "\f1ec",
"voice-chat": "\f1ed",
"volume-1": "\f1ee",
"volume-2": "\f1ef",
"volume-3": "\f1f0",
"web": "\f1f1",
"webapp": "\f1f2",
"word-wrap": "\f1f3",
"zoom-in": "\f1f4",
"zoom-out": "\f1f5",
"auction": "\f114",
"author-hidden": "\f115",
"avatar-archived-chats": "\f116",
"avatar-deleted-account": "\f117",
"avatar-saved-messages": "\f118",
"bold": "\f119",
"boost-outline": "\f11a",
"boost": "\f11b",
"boostcircle": "\f11c",
"boosts": "\f11d",
"bot-command": "\f11e",
"bot-commands-filled": "\f11f",
"bots": "\f120",
"bug": "\f121",
"calendar-filter": "\f122",
"calendar": "\f123",
"camera-add": "\f124",
"camera": "\f125",
"car": "\f126",
"card": "\f127",
"cash-circle": "\f128",
"channel-filled": "\f129",
"channel": "\f12a",
"channelviews": "\f12b",
"chat-badge": "\f12c",
"chats-badge": "\f12d",
"check": "\f12e",
"clock-edit": "\f12f",
"clock": "\f130",
"close-circle": "\f131",
"close-topic": "\f132",
"close": "\f133",
"cloud-download": "\f134",
"collapse-modal": "\f135",
"collapse": "\f136",
"colorize": "\f137",
"comments-sticker": "\f138",
"comments": "\f139",
"copy-media": "\f13a",
"copy": "\f13b",
"darkmode": "\f13c",
"data": "\f13d",
"delete-filled": "\f13e",
"delete-left": "\f13f",
"delete-user": "\f140",
"delete": "\f141",
"diamond": "\f142",
"document": "\f143",
"double-badge": "\f144",
"down": "\f145",
"download": "\f146",
"eats": "\f147",
"edit": "\f148",
"email": "\f149",
"enter": "\f14a",
"expand-modal": "\f14b",
"expand": "\f14c",
"eye-closed-outline": "\f14d",
"eye-closed": "\f14e",
"eye-outline": "\f14f",
"eye": "\f150",
"favorite-filled": "\f151",
"favorite": "\f152",
"file-badge": "\f153",
"flag": "\f154",
"folder-badge": "\f155",
"folder": "\f156",
"fontsize": "\f157",
"forums": "\f158",
"forward": "\f159",
"fullscreen": "\f15a",
"gifs": "\f15b",
"gift": "\f15c",
"group-filled": "\f15d",
"group": "\f15e",
"grouped-disable": "\f15f",
"grouped": "\f160",
"hand-stop": "\f161",
"hashtag": "\f162",
"heart-outline": "\f163",
"heart": "\f164",
"help": "\f165",
"info-filled": "\f166",
"info": "\f167",
"install": "\f168",
"italic": "\f169",
"key": "\f16a",
"keyboard": "\f16b",
"lamp": "\f16c",
"language": "\f16d",
"large-pause": "\f16e",
"large-play": "\f16f",
"link-badge": "\f170",
"link-broken": "\f171",
"link": "\f172",
"location": "\f173",
"lock-badge": "\f174",
"lock": "\f175",
"logout": "\f176",
"loop": "\f177",
"mention": "\f178",
"message-failed": "\f179",
"message-pending": "\f17a",
"message-read": "\f17b",
"message-succeeded": "\f17c",
"message": "\f17d",
"microphone-alt": "\f17e",
"microphone": "\f17f",
"monospace": "\f180",
"more-circle": "\f181",
"more": "\f182",
"move-caption-down": "\f183",
"move-caption-up": "\f184",
"mute": "\f185",
"muted": "\f186",
"my-notes": "\f187",
"new-chat-filled": "\f188",
"next": "\f189",
"nochannel": "\f18a",
"noise-suppression": "\f18b",
"non-contacts": "\f18c",
"one-filled": "\f18d",
"open-in-new-tab": "\f18e",
"password-off": "\f18f",
"pause": "\f190",
"permissions": "\f191",
"phone-discard-outline": "\f192",
"phone-discard": "\f193",
"phone": "\f194",
"photo": "\f195",
"pin-badge": "\f196",
"pin-list": "\f197",
"pin": "\f198",
"pinned-chat": "\f199",
"pinned-message": "\f19a",
"pip": "\f19b",
"play-story": "\f19c",
"play": "\f19d",
"poll": "\f19e",
"previous": "\f19f",
"privacy-policy": "\f1a0",
"quote-text": "\f1a1",
"quote": "\f1a2",
"readchats": "\f1a3",
"recent": "\f1a4",
"reload": "\f1a5",
"remove-quote": "\f1a6",
"remove": "\f1a7",
"reopen-topic": "\f1a8",
"replace": "\f1a9",
"replies": "\f1aa",
"reply-filled": "\f1ab",
"reply": "\f1ac",
"revenue-split": "\f1ad",
"revote": "\f1ae",
"save-story": "\f1af",
"saved-messages": "\f1b0",
"schedule": "\f1b1",
"search": "\f1b2",
"select": "\f1b3",
"send-outline": "\f1b4",
"send": "\f1b5",
"settings-filled": "\f1b6",
"settings": "\f1b7",
"share-filled": "\f1b8",
"share-screen-outlined": "\f1b9",
"share-screen-stop": "\f1ba",
"share-screen": "\f1bb",
"show-message": "\f1bc",
"sidebar": "\f1bd",
"skip-next": "\f1be",
"skip-previous": "\f1bf",
"smallscreen": "\f1c0",
"smile": "\f1c1",
"sort": "\f1c2",
"speaker-muted-story": "\f1c3",
"speaker-outline": "\f1c4",
"speaker-story": "\f1c5",
"speaker": "\f1c6",
"spoiler-disable": "\f1c7",
"spoiler": "\f1c8",
"sport": "\f1c9",
"star": "\f1ca",
"stars-lock": "\f1cb",
"stats": "\f1cc",
"stealth-future": "\f1cd",
"stealth-past": "\f1ce",
"stickers": "\f1cf",
"stop-raising-hand": "\f1d0",
"stop": "\f1d1",
"story-caption": "\f1d2",
"story-expired": "\f1d3",
"story-priority": "\f1d4",
"story-reply": "\f1d5",
"strikethrough": "\f1d6",
"tag-add": "\f1d7",
"tag-crossed": "\f1d8",
"tag-filter": "\f1d9",
"tag-name": "\f1da",
"tag": "\f1db",
"timer": "\f1dc",
"toncoin": "\f1dd",
"trade": "\f1de",
"transcribe": "\f1df",
"truck": "\f1e0",
"unarchive": "\f1e1",
"underlined": "\f1e2",
"unlock-badge": "\f1e3",
"unlock": "\f1e4",
"unmute": "\f1e5",
"unpin": "\f1e6",
"unread": "\f1e7",
"up": "\f1e8",
"user-filled": "\f1e9",
"user-online": "\f1ea",
"user": "\f1eb",
"video-outlined": "\f1ec",
"video-stop": "\f1ed",
"video": "\f1ee",
"view-once": "\f1ef",
"voice-chat": "\f1f0",
"volume-1": "\f1f1",
"volume-2": "\f1f2",
"volume-3": "\f1f3",
"web": "\f1f4",
"webapp": "\f1f5",
"word-wrap": "\f1f6",
"zoom-in": "\f1f7",
"zoom-out": "\f1f8",
);
.icon-active-sessions::before {
@ -333,6 +336,9 @@ $icons-map: (
.icon-attach::before {
content: map.get($icons-map, "attach");
}
.icon-auction::before {
content: map.get($icons-map, "auction");
}
.icon-author-hidden::before {
content: map.get($icons-map, "author-hidden");
}
@ -468,6 +474,9 @@ $icons-map: (
.icon-delete::before {
content: map.get($icons-map, "delete");
}
.icon-diamond::before {
content: map.get($icons-map, "diamond");
}
.icon-document::before {
content: map.get($icons-map, "document");
}
@ -933,6 +942,9 @@ $icons-map: (
.icon-toncoin::before {
content: map.get($icons-map, "toncoin");
}
.icon-trade::before {
content: map.get($icons-map, "trade");
}
.icon-transcribe::before {
content: map.get($icons-map, "transcribe");
}

Binary file not shown.

Binary file not shown.

View File

@ -18,6 +18,7 @@ export type FontIconName =
| 'arrow-right'
| 'ask-support'
| 'attach'
| 'auction'
| 'author-hidden'
| 'avatar-archived-chats'
| 'avatar-deleted-account'
@ -63,6 +64,7 @@ export type FontIconName =
| 'delete-left'
| 'delete-user'
| 'delete'
| 'diamond'
| 'document'
| 'double-badge'
| 'down'
@ -218,6 +220,7 @@ export type FontIconName =
| 'tag'
| 'timer'
| 'toncoin'
| 'trade'
| 'transcribe'
| 'truck'
| 'unarchive'

View File

@ -565,6 +565,7 @@ export type StarGiftInfo = {
gift: ApiStarGiftRegular;
shouldHideName?: boolean;
message?: ApiFormattedText;
shouldUpgrade?: boolean;
};
export interface TabThread {

View File

@ -1164,15 +1164,15 @@ export interface LangPair {
'GiftInfoSent': undefined;
'GiftInfoReceived': undefined;
'GiftInfoTitle': undefined;
'GiftInfoDescriptionFreeUpgrade': undefined;
'GiftInfoDescriptionUpgraded': undefined;
'GiftInfoFrom': undefined;
'GiftInfoDate': undefined;
'GiftInfoValue': undefined;
'GiftInfoMakeVisible': undefined;
'GiftInfoMakeInvisible': undefined;
'GiftInfoConvertTitle': undefined;
'GiftInfoConvertDescription2': undefined;
'GiftInfoSavedView': undefined;
'GiftInfoHidden': undefined;
'GiftInfoSavedHide': undefined;
'GiftInfoSavedShow': undefined;
'GiftInfoAvailability': undefined;
'GiftInfoFirstSale': undefined;
'GiftInfoLastSale': undefined;
@ -1186,6 +1186,21 @@ export interface LangPair {
'GiftInfoStatus': undefined;
'GiftInfoStatusNonUnique': undefined;
'GiftInfoViewUpgraded': undefined;
'GiftInfoUpgradeBadge': undefined;
'GiftInfoUpgradeForFree': undefined;
'GiftUpgradeUniqueTitle': undefined;
'GiftUpgradeUniqueDescription': undefined;
'GiftUpgradeTransferableTitle': undefined;
'GiftUpgradeTransferableDescription': undefined;
'GiftUpgradeTradeableTitle': undefined;
'GiftUpgradeTradeableDescription': undefined;
'GiftUpgradeTitle': undefined;
'GiftUpgradeTextOwn': undefined;
'GiftUpgradeKeepDetails': undefined;
'GiftUpgradedTitle': undefined;
'GiftUpgradedDescription': undefined;
'GiftMakeUniqueAcc': undefined;
'GiftMakeUniqueLink': undefined;
'AllGiftsCategory': undefined;
'LimitedGiftsCategory': undefined;
'StockGiftsCategory': undefined;
@ -1200,12 +1215,6 @@ export interface LangPair {
'GiftFrom': undefined;
'ReceivedGift': undefined;
'SentGift': undefined;
'StarGiftInfoLinkCaption': undefined;
'StarGiftDisplayOnMyPage': undefined;
'StarGiftConvertTo': undefined;
'StarGiftHideFromMyPage': undefined;
'StarGiftSenderPrivacyNote': undefined;
'StarGiftAvailability': undefined;
'StarsSubscribeInfoLinkText': undefined;
'StarsSubscribeInfoLink': undefined;
'StarsBalance': undefined;
@ -1629,6 +1638,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'GiftSend': {
'amount': V;
};
'GiftInfoDescriptionFreeUpgradeOut': {
'user': V;
};
'GiftInfoConvertDescription1': {
'user': V;
'amount': V;
@ -1636,6 +1648,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'GiftInfoSaved': {
'link': V;
};
'GiftInfoHidden': {
'link': V;
};
'GiftInfoIssued': {
'issued': V;
'total': V;
@ -1663,6 +1678,19 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'date': V;
'text': V;
};
'GiftUpgradeText': {
'peer': V;
};
'GiftUpgradeButton': {
'amount': V;
};
'GiftMakeUnique': {
'stars': V;
};
'GiftMakeUniqueDescription': {
'user': V;
'link': V;
};
'StarsAmount': {
'amount': V;
};
@ -1695,19 +1723,6 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'ActionStarGiftOutDescriptionUpgrade': {
'user': V;
};
'StarGiftInfoDescriptionInbound': {
'count': V;
'link': V;
};
'StarGiftInfoDescriptionOutgoing': {
'user': V;
'count': V;
'link': V;
};
'StarGiftAvailabilityValue': {
'number': V;
'total': V;
};
'StarsSubscribeInfo': {
'link': V;
};
@ -1917,6 +1932,9 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
'user': V;
'amount': V;
};
'GiftInfoDescriptionUpgrade': {
'amount': V;
};
'GiftInfoDescriptionConverted': {
'amount': V;
};

View File

@ -3,6 +3,7 @@ import React from '../../lib/teact/teact';
import type { LangFn } from './types';
import { STARS_ICON_PLACEHOLDER } from '../../config';
import buildClassName from '../buildClassName';
import Icon from '../../components/common/icons/Icon';
import StarIcon from '../../components/common/icons/StarIcon';
@ -11,10 +12,11 @@ export function formatStarsAsText(lang: LangFn, amount: number) {
return lang('StarsAmountText', { amount }, { pluralValue: amount });
}
export function formatStarsAsIcon(lang: LangFn, amount: number, asFont?: boolean) {
export function formatStarsAsIcon(lang: LangFn, amount: number, options?: { asFont?: boolean; className?: string }) {
const { asFont, className } = options || {};
const icon = asFont
? <Icon name="star" className="star-amount-icon" />
: <StarIcon type="gold" className="star-amount-icon" size="adaptive" />;
? <Icon name="star" className={buildClassName('star-amount-icon', className)} />
: <StarIcon type="gold" className={buildClassName('star-amount-icon', className)} size="adaptive" />;
return lang('StarsAmount', { amount }, {
withNodes: true,
specialReplacement: {

View File

@ -359,8 +359,8 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
body = renderActionMessageText(
oldTranslate,
message,
!isChat ? messageSenderUser : undefined,
isChat ? chat : undefined,
messageSenderUser,
chat,
actionTargetUsers,
actionTargetMessage,
actionTargetChatId,