Gifts Modal: Implement extended gift options (#5017)

Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2024-11-02 21:11:10 +04:00
parent 23375d3175
commit 1dc29627bd
121 changed files with 4341 additions and 1923 deletions

View File

@ -84,6 +84,7 @@ export interface GramJsAppConfig extends LimitsConfig {
upload_premium_speedup_download?: number;
upload_premium_speedup_upload?: number;
stars_gifts_enabled?: boolean;
stargifts_message_length_max?: number;
}
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -166,6 +167,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
channelRestrictAdsLevelMin: appConfig.channel_restrict_sponsored_level_min,
paidReactionMaxAmount: appConfig.stars_paid_reaction_amount_max,
isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled,
isStarsGiftsEnabled: appConfig.stars_gifts_enabled,
isStarsGiftEnabled: appConfig.stars_gifts_enabled,
starGiftMaxMessageLength: appConfig.stargifts_message_length_max,
};
}

View File

@ -8,9 +8,9 @@ import type {
ApiGame,
ApiGiveaway,
ApiGiveawayResults,
ApiInvoice,
ApiLocation,
ApiMediaExtendedPreview,
ApiMediaInvoice,
ApiMessageStoryData,
ApiPaidMedia,
ApiPhoto,
@ -473,12 +473,12 @@ function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined
return buildPoll(media.poll, media.results);
}
function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiInvoice | undefined {
function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiMediaInvoice | undefined {
if (!(media instanceof GramJs.MessageMediaInvoice)) {
return undefined;
}
return buildInvoice(media);
return buildMediaInvoice(media);
}
function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | undefined {
@ -671,9 +671,9 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A
};
}
export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
export function buildMediaInvoice(media: GramJs.MessageMediaInvoice): ApiMediaInvoice {
const {
description: text, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia,
description, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia,
} = media;
const preview = extendedMedia instanceof GramJs.MessageExtendedMediaPreview
@ -682,10 +682,10 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
return {
mediaType: 'invoice',
title,
text,
description,
photo: buildApiWebDocument(photo),
receiptMsgId,
amount: Number(totalAmount),
receiptMessageId: receiptMsgId,
amount: totalAmount.toJSNumber(),
currency,
isTest: test,
extendedMedia: preview,

View File

@ -7,11 +7,13 @@ import type {
ApiChat,
ApiContact,
ApiFactCheck,
ApiFormattedText,
ApiGroupCall,
ApiInputMessageReplyInfo,
ApiInputReplyInfo,
ApiKeyboardButton,
ApiMessage,
ApiMessageActionStarGift,
ApiMessageEntity,
ApiMessageForwardInfo,
ApiNewPoll,
@ -38,6 +40,7 @@ import {
DELETED_COMMENTS_CHANNEL_ID,
SERVICE_NOTIFICATIONS_USER_ID,
SPONSORED_MESSAGE_CACHE_MS,
STARS_CURRENCY_CODE,
SUPPORTED_AUDIO_CONTENT_TYPES,
SUPPORTED_PHOTO_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
@ -59,6 +62,7 @@ import {
buildApiPhoto,
} from './common';
import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiStarGift } from './payments';
import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildMessageReactions } from './reactions';
@ -349,6 +353,21 @@ export function buildApiFactCheck(factCheck: GramJs.FactCheck): ApiFactCheck {
};
}
function buildApiMessageActionStarGift(action: GramJs.MessageActionStarGift) : ApiMessageActionStarGift {
const {
nameHidden, saved, converted, gift, message, convertStars,
} = action;
return {
isNameHidden: Boolean(nameHidden),
isSaved: Boolean(saved),
isConverted: Boolean(converted),
gift: buildApiStarGift(gift),
message: message && buildApiFormattedText(message),
starsToConvert: convertStars.toJSNumber(),
};
}
function buildAction(
action: GramJs.TypeMessageAction,
senderId: string | undefined,
@ -364,6 +383,7 @@ function buildAction(
let call: Partial<ApiGroupCall> | undefined;
let amount: number | undefined;
let stars: number | undefined;
let starGift: ApiMessageActionStarGift | undefined;
let currency: string | undefined;
let giftCryptoInfo: {
currency: string;
@ -382,6 +402,7 @@ function buildAction(
let isUnclaimed: boolean | undefined;
let pluralValue: number | undefined;
let transactionId: string | undefined;
let message: ApiFormattedText | undefined;
let targetUserIds = 'users' in action
? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
@ -528,6 +549,9 @@ function buildAction(
} else {
translationValues.push('%action_origin%', '%gift_payment_amount%');
}
if (action.message) {
message = buildApiFormattedText(action.message);
}
if (targetPeerId) {
targetUserIds.push(targetPeerId);
}
@ -584,6 +608,10 @@ function buildAction(
if (isOutgoing) {
translationValues.push('%gift_payment_amount%');
}
if (action.message) {
message = buildApiFormattedText(action.message);
}
currency = action.currency;
if (action.cryptoCurrency) {
giftCryptoInfo = {
@ -667,6 +695,24 @@ function buildAction(
amount = action.amount.toJSNumber();
stars = action.stars.toJSNumber();
transactionId = action.transactionId;
} else if (action instanceof GramJs.MessageActionStarGift) {
type = 'starGift';
if (isOutgoing) {
text = 'ActionGiftOutbound';
translationValues.push('%gift_payment_amount%');
} else {
text = 'ActionGiftInbound';
translationValues.push('%action_origin%', '%gift_payment_amount%');
}
if (targetPeerId) {
targetUserIds.push(targetPeerId);
targetChatId = targetPeerId;
}
amount = action.gift.stars.toJSNumber();
currency = STARS_CURRENCY_CODE;
starGift = buildApiMessageActionStarGift(action);
} else {
text = 'ChatList.UnsupportedMessage';
}
@ -685,6 +731,7 @@ function buildAction(
photo, // TODO Only used internally now, will be used for the UI in future
amount,
stars,
starGift,
currency,
giftCryptoInfo,
isGiveaway,
@ -699,6 +746,7 @@ function buildAction(
isUnclaimed,
pluralValue,
transactionId,
message,
};
}

View File

@ -1,3 +1,4 @@
import bigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiPremiumSection } from '../../../global/types';
@ -18,18 +19,20 @@ import type {
ApiPrepaidGiveaway,
ApiPrepaidStarsGiveaway,
ApiReceipt,
ApiStarGift,
ApiStarGiveawayOption,
ApiStarsGiveawayWinnerOption,
ApiStarsSubscription,
ApiStarsTransaction,
ApiStarsTransactionPeer,
ApiStarTopupOption,
ApiUserStarGift,
BoughtPaidMedia,
} from '../../types';
import { addWebDocumentToLocalDb } from '../helpers';
import { buildApiStarsSubscriptionPricing } from './chats';
import { buildApiMessageEntity } from './common';
import { buildApiFormattedText, buildApiMessageEntity } from './common';
import { omitVirtualClassFields } from './helpers';
import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
@ -64,26 +67,20 @@ export function buildApiReceipt(receipt: GramJs.payments.TypePaymentReceipt): Ap
if (receipt instanceof GramJs.payments.PaymentReceiptStars) {
const {
botId, currency, date, description: text, title, totalAmount, transactionId,
botId, currency, date, description, title, totalAmount, transactionId, invoice,
} = receipt;
if (photo) {
addWebDocumentToLocalDb(photo);
}
return {
type: 'stars',
currency,
peer: {
type: 'peer',
id: buildApiPeerId(botId, 'user'),
},
date,
text,
botId: buildApiPeerId(botId, 'user'),
description,
title,
totalAmount: -totalAmount.toJSNumber(),
transactionId,
photo: photo && buildApiWebDocument(photo),
photo: buildApiWebDocument(photo),
invoice: buildApiInvoice(invoice),
};
}
@ -91,22 +88,19 @@ export function buildApiReceipt(receipt: GramJs.payments.TypePaymentReceipt): Ap
invoice,
info,
shipping,
currency,
totalAmount,
credentialsTitle,
tipAmount,
title,
description: text,
description,
botId,
currency,
date,
providerId,
} = receipt;
const { shippingAddress, phone, name } = (info || {});
const { prices } = invoice;
const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({
label,
amount: amount.toJSNumber(),
}));
let shippingPrices: ApiLabeledPrice[] | undefined;
let shippingMethod: string | undefined;
@ -122,32 +116,50 @@ export function buildApiReceipt(receipt: GramJs.payments.TypePaymentReceipt): Ap
return {
type: 'regular',
currency,
prices: mappedPrices,
info: { shippingAddress, phone, name },
totalAmount: totalAmount.toJSNumber(),
currency,
date,
credentialsTitle,
shippingPrices,
shippingMethod,
tipAmount: tipAmount ? tipAmount.toJSNumber() : 0,
title,
text,
description,
botId: buildApiPeerId(botId, 'user'),
providerId: providerId.toString(),
photo: photo && buildApiWebDocument(photo),
invoice: buildApiInvoice(invoice),
};
}
export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiPaymentForm | undefined {
export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiPaymentForm {
if (form instanceof GramJs.payments.PaymentFormStarGift) {
return undefined;
const { formId } = form;
return {
type: 'stargift',
formId: String(formId),
invoice: buildApiInvoice(form.invoice),
};
}
if (form instanceof GramJs.payments.PaymentFormStars) {
const { botId, formId } = form;
const {
botId, formId, title, description, photo,
} = form;
if (photo) {
addWebDocumentToLocalDb(photo);
}
return {
type: 'stars',
botId: buildApiPeerId(botId, 'user'),
formId: String(formId),
title,
description,
photo: buildApiWebDocument(photo),
invoice: buildApiInvoice(form.invoice),
};
}
@ -163,25 +175,14 @@ export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiP
savedCredentials,
url,
botId,
description,
title,
photo,
} = form;
const {
test: isTest,
nameRequested: isNameRequested,
phoneRequested: isPhoneRequested,
emailRequested: isEmailRequested,
shippingAddressRequested: isShippingAddressRequested,
flexible: isFlexible,
phoneToProvider: shouldSendPhoneToProvider,
emailToProvider: shouldSendEmailToProvider,
currency,
prices,
} = invoice;
const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({
label,
amount: amount.toJSNumber(),
}));
if (photo) {
addWebDocumentToLocalDb(photo);
}
const { shippingAddress } = savedInfo || {};
const cleanedInfo: ApiPaymentSavedInfo | undefined = savedInfo ? omitVirtualClassFields(savedInfo) : undefined;
if (cleanedInfo && shippingAddress) {
@ -192,6 +193,9 @@ export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiP
return {
type: 'regular',
title,
description,
photo: buildApiWebDocument(photo),
url,
botId: buildApiPeerId(botId, 'user'),
canSaveCredentials,
@ -200,18 +204,7 @@ export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiP
providerId: String(providerId),
nativeProvider,
savedInfo: cleanedInfo,
invoiceContainer: {
isTest,
isNameRequested,
isPhoneRequested,
isEmailRequested,
isShippingAddressRequested,
isFlexible,
shouldSendPhoneToProvider,
shouldSendEmailToProvider,
currency,
prices: mappedPrices,
},
invoice: buildApiInvoice(invoice),
nativeParams: {
needCardholderName: Boolean(nativeData?.need_cardholder_name),
needCountry: Boolean(nativeData?.need_country),
@ -224,32 +217,47 @@ export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiP
};
}
export function buildApiInvoiceFromForm(form: GramJs.payments.TypePaymentForm): ApiInvoice | undefined {
if (form instanceof GramJs.payments.PaymentFormStarGift) {
return undefined;
}
export function buildApiInvoice(invoice: GramJs.Invoice): ApiInvoice {
const {
invoice, description: text, title, photo,
} = form;
const {
test, currency, prices, recurring, termsUrl, maxTipAmount, suggestedTipAmounts,
test,
currency,
prices,
recurring,
termsUrl,
maxTipAmount,
suggestedTipAmounts,
emailRequested,
emailToProvider,
nameRequested,
phoneRequested,
phoneToProvider,
shippingAddressRequested,
flexible,
} = invoice;
const totalAmount = prices.reduce((ac, cur) => ac + cur.amount.toJSNumber(), 0);
const mappedPrices: ApiLabeledPrice[] = prices.map(({ label, amount }) => ({
label,
amount: amount.toJSNumber(),
}));
const totalAmount = prices.reduce((acc, cur) => acc.add(cur.amount), bigInt(0)).toJSNumber();
return {
mediaType: 'invoice',
text,
title,
photo: buildApiWebDocument(photo),
amount: totalAmount,
totalAmount,
currency,
isTest: test,
isRecurring: recurring,
termsUrl,
prices: mappedPrices,
maxTipAmount: maxTipAmount?.toJSNumber(),
...(suggestedTipAmounts && { suggestedTipAmounts: suggestedTipAmounts.map((tip) => tip.toJSNumber()) }),
suggestedTipAmounts: suggestedTipAmounts?.map((tip) => tip.toJSNumber()),
isEmailRequested: emailRequested,
isEmailSentToProvider: emailToProvider,
isNameRequested: nameRequested,
isPhoneRequested: phoneRequested,
isPhoneSentToProvider: phoneToProvider,
isShippingAddressRequested: shippingAddressRequested,
isFlexible: flexible,
};
}
@ -514,7 +522,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
const {
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
subscriptionPeriod,
subscriptionPeriod, stargift, giveawayPostId,
} = transaction;
if (photo) {
@ -540,6 +548,8 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
extendedMedia: boughtExtendedMedia,
subscriptionPeriod,
isReaction: reaction,
starGift: stargift && buildApiStarGift(stargift),
giveawayPostId,
};
}
@ -572,3 +582,36 @@ export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): Ap
isExtended: extended,
};
}
export function buildApiStarGift(startGift: GramJs.StarGift): ApiStarGift {
const {
id, limited, sticker, stars, availabilityRemains, availabilityTotal, convertStars,
} = startGift;
return {
id: id.toString(),
isLimited: limited,
stickerId: sticker.id.toString(),
stars: stars.toJSNumber(),
availabilityRemains,
availabilityTotal,
starsToConvert: convertStars.toJSNumber(),
};
}
export function buildApiUserStarGift(userStarGift: GramJs.UserStarGift): ApiUserStarGift {
const {
gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved,
} = userStarGift;
return {
gift: buildApiStarGift(gift),
date,
starsToConvert: convertStars?.toJSNumber(),
fromId: fromId && buildApiPeerId(fromId, 'user'),
message: message && buildApiFormattedText(message),
messageId: msgId,
isNameHidden: nameHidden,
isUnsaved: unsaved,
};
}

View File

@ -22,7 +22,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
profilePhoto, voiceMessagesForbidden, premiumGifts,
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled,
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount,
},
users,
} = mtpUserFull;
@ -50,6 +50,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
personalChannelId: personalChannelId && buildApiPeerId(personalChannelId, 'channel'),
personalChannelMessageId: personalChannelMessage,
areAdsEnabled: sponsoredEnabled,
starGiftCount: stargiftsCount,
};
}

View File

@ -573,6 +573,7 @@ GramJs.TypeInputStorePaymentPurpose {
: undefined,
currency: purpose.currency,
amount: BigInt(purpose.amount),
message: purpose.message && buildInputTextWithEntities(purpose.message),
});
}
@ -633,6 +634,18 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
});
}
case 'stargift': {
const {
user, shouldHideName, giftId, message,
} = invoice;
return new GramJs.InputInvoiceStarGift({
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
hideName: shouldHideName || undefined,
giftId: BigInt(giftId),
message: message && buildInputTextWithEntities(message),
});
}
case 'stars': {
const purpose = buildInputStorePaymentPurpose(invoice.purpose);
return new GramJs.InputInvoiceStars({

View File

@ -3,7 +3,8 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiChat, ApiInputStorePaymentPurpose, ApiPeer, ApiRequestInputInvoice,
ApiThemeParameters,
ApiSticker, ApiThemeParameters,
ApiUser,
} from '../../types';
import { DEBUG } from '../../../config';
@ -12,25 +13,26 @@ import {
buildApiBoostsStatus,
buildApiCheckedGiftCode,
buildApiGiveawayInfo,
buildApiInvoiceFromForm,
buildApiMyBoost,
buildApiPaymentForm,
buildApiPremiumGiftCodeOption,
buildApiPremiumPromo,
buildApiReceipt,
buildApiStarGift,
buildApiStarsGiftOptions,
buildApiStarsGiveawayOptions,
buildApiStarsSubscription,
buildApiStarsTransaction,
buildApiStarTopupOption,
buildApiUserStarGift,
buildShippingOptions,
} from '../apiBuilders/payments';
import { buildApiPeerId } from '../apiBuilders/peers';
import { buildStickerFromDocument } from '../apiBuilders/symbols';
import {
buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo,
} from '../gramjsBuilders';
import {
addWebDocumentToLocalDb,
deserializeBytes,
serializeBytes,
} from '../helpers';
@ -171,23 +173,35 @@ export async function sendStarPaymentForm({
}
export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice, theme?: ApiThemeParameters) {
const result = await invokeRequest(new GramJs.payments.GetPaymentForm({
invoice: buildInputInvoice(inputInvoice),
themeParams: theme ? buildInputThemeParams(theme) : undefined,
}));
try {
const result = await invokeRequest(new GramJs.payments.GetPaymentForm({
invoice: buildInputInvoice(inputInvoice),
themeParams: theme ? buildInputThemeParams(theme) : undefined,
}), {
shouldThrow: true,
});
if (!result || result instanceof GramJs.payments.PaymentFormStarGift) {
if (!result) {
return undefined;
}
return buildApiPaymentForm(result);
} catch (err) {
if (err instanceof Error) {
// Can be removed if separate error handling is added to payment UI
sendApiUpdate({
'@type': 'error',
error: {
message: err.message,
hasErrorKey: true,
},
});
return {
error: err.message,
};
}
return undefined;
}
if (result.photo) {
addWebDocumentToLocalDb(result.photo);
}
return {
form: buildApiPaymentForm(result)!,
invoice: buildApiInvoiceFromForm(result)!,
};
}
export async function getReceipt(chat: ApiChat, msgId: number) {
@ -408,6 +422,86 @@ export async function fetchStarsGiveawayOptions() {
return result.map(buildApiStarsGiveawayOptions);
}
export async function fetchStarGifts() {
const result = await invokeRequest(new GramJs.payments.GetStarGifts({}));
if (!result || result instanceof GramJs.payments.StarGiftsNotModified) {
return undefined;
}
const gifts = result.gifts.map(buildApiStarGift);
const stickers : Record<string, ApiSticker> = {};
result.gifts.forEach((gift) => {
if (gift.sticker instanceof GramJs.Document) {
localDb.documents[String(gift.sticker.id)] = gift.sticker;
}
const sticker = buildStickerFromDocument(gift.sticker);
if (sticker) {
stickers[sticker.id] = sticker;
}
});
return { gifts, stickers };
}
export async function fetchUserStarGifts({
user,
offset = '',
limit,
}: {
user: ApiUser;
offset?: string;
limit?: number;
}) {
const result = await invokeRequest(new GramJs.payments.GetUserStarGifts({
userId: buildInputPeer(user.id, user.accessHash),
offset,
limit,
}));
if (!result) {
return undefined;
}
const gifts = result.gifts.map(buildApiUserStarGift);
return {
gifts,
nextOffset: result.nextOffset,
};
}
export function saveStarGift({
user,
messageId,
shouldUnsave,
}: {
user: ApiUser;
messageId: number;
shouldUnsave?: boolean;
}) {
return invokeRequest(new GramJs.payments.SaveStarGift({
userId: buildInputPeer(user.id, user.accessHash),
msgId: messageId,
unsave: shouldUnsave || undefined,
}));
}
export function convertStarGift({
user,
messageId,
}: {
user: ApiUser;
messageId: number;
}) {
return invokeRequest(new GramJs.payments.ConvertStarGift({
userId: buildInputPeer(user.id, user.accessHash),
msgId: messageId,
}));
}
export function launchPrepaidGiveaway({
chat,
giveawayId,

View File

@ -3,12 +3,14 @@ import type { ThreadId } from '../../types';
import type { ApiWebDocument } from './bots';
import type { ApiGroupCall, PhoneCallAction } from './calls';
import type { ApiChat, ApiPeerColor } from './chats';
import type { ApiChatInviteInfo } from './misc';
import type {
ApiInputStorePaymentPurpose,
ApiLabeledPrice,
ApiPremiumGiftCodeOption,
ApiStarGift,
} from './payments';
import type { ApiMessageStoryData, ApiWebPageStickerData, ApiWebPageStoryData } from './stories';
import type { ApiUser } from './users';
export interface ApiDimensions {
width: number;
@ -233,6 +235,7 @@ export type ApiInputInvoiceGiftCode = {
currency: string;
amount: number;
option: ApiPremiumGiftCodeOption;
message?: ApiFormattedText;
};
export type ApiInputInvoiceStars = {
@ -250,6 +253,14 @@ export type ApiInputInvoiceStarsGift = {
amount: number;
};
export type ApiInputInvoiceStarGift = {
type: 'stargift';
shouldHideName?: boolean;
userId: string;
giftId: string;
message?: ApiFormattedText;
};
export type ApiInputInvoiceStarsGiveaway = {
type: 'starsgiveaway';
chatId: string;
@ -268,12 +279,11 @@ export type ApiInputInvoiceStarsGiveaway = {
export type ApiInputInvoiceChatInviteSubscription = {
type: 'chatInviteSubscription';
hash: string;
inviteInfo: ApiChatInviteInfo;
};
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
| ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars | ApiInputInvoiceStarsGiveaway
| ApiInputInvoiceChatInviteSubscription;
| ApiInputInvoiceGiftCode | ApiInputInvoiceStars | ApiInputInvoiceStarsGift
| ApiInputInvoiceStarsGiveaway | ApiInputInvoiceStarGift | ApiInputInvoiceChatInviteSubscription;
/* Used for Invoice request */
export type ApiRequestInputInvoiceMessage = {
@ -303,6 +313,14 @@ export type ApiRequestInputInvoiceStarsGiveaway = {
purpose: ApiInputStorePaymentPurpose;
};
export type ApiRequestInputInvoiceStarGift = {
type: 'stargift';
shouldHideName?: boolean;
user: ApiUser;
giftId: string;
message?: ApiFormattedText;
};
export type ApiRequestInputInvoiceChatInviteSubscription = {
type: 'chatInviteSubscription';
hash: string;
@ -310,22 +328,36 @@ export type ApiRequestInputInvoiceChatInviteSubscription = {
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway
| ApiRequestInputInvoiceChatInviteSubscription;
| ApiRequestInputInvoiceChatInviteSubscription | ApiRequestInputInvoiceStarGift;
export interface ApiInvoice {
mediaType: 'invoice';
text: string;
title: string;
photo?: ApiWebDocument;
amount: number;
prices: ApiLabeledPrice[];
totalAmount: number;
currency: string;
receiptMsgId?: number;
isTest?: boolean;
isRecurring?: boolean;
termsUrl?: string;
extendedMedia?: ApiMediaExtendedPreview;
maxTipAmount?: number;
suggestedTipAmounts?: number[];
isNameRequested?: boolean;
isPhoneRequested?: boolean;
isEmailRequested?: boolean;
isShippingAddressRequested?: boolean;
isFlexible?: boolean;
isPhoneSentToProvider?: boolean;
isEmailSentToProvider?: boolean;
}
export interface ApiMediaInvoice {
mediaType: 'invoice';
title: string;
description: string;
photo?: ApiWebDocument;
isTest?: boolean;
receiptMessageId?: number;
currency: string;
amount: number;
extendedMedia?: ApiMediaExtendedPreview;
}
export interface ApiMediaExtendedPreview {
@ -420,6 +452,15 @@ export type ApiNewPoll = {
};
};
export interface ApiMessageActionStarGift {
isNameHidden: boolean;
isSaved: boolean;
isConverted?: boolean;
gift: ApiStarGift;
message?: ApiFormattedText;
starsToConvert: number;
}
export interface ApiAction {
mediaType: 'action';
text: string;
@ -438,6 +479,7 @@ export interface ApiAction {
| 'giftPremium'
| 'giftCode'
| 'prizeStars'
| 'starGift'
| 'other';
photo?: ApiPhoto;
amount?: number;
@ -448,6 +490,7 @@ export interface ApiAction {
currency: string;
amount: number;
};
starGift?: ApiMessageActionStarGift;
translationValues: string[];
call?: Partial<ApiGroupCall>;
phoneCall?: PhoneCallAction;
@ -459,6 +502,7 @@ export interface ApiAction {
isGiveaway?: boolean;
isUnclaimed?: boolean;
pluralValue?: number;
message?: ApiFormattedText;
}
export interface ApiWebPage {
@ -627,7 +671,7 @@ export type MediaContent = {
webPage?: ApiWebPage;
audio?: ApiAudio;
voice?: ApiVoice;
invoice?: ApiInvoice;
invoice?: ApiMediaInvoice;
location?: ApiLocation;
game?: ApiGame;
storyData?: ApiMessageStoryData;

View File

@ -235,7 +235,8 @@ export interface ApiAppConfig {
channelRestrictAdsLevelMin?: number;
paidReactionMaxAmount?: number;
isChannelRevenueWithdrawalEnabled?: boolean;
isStarsGiftsEnabled?: boolean;
isStarsGiftEnabled?: boolean;
starGiftMaxMessageLength?: number;
}
export interface ApiConfig {

View File

@ -1,9 +1,13 @@
import type { ApiPremiumSection } from '../../global/types';
import type { ApiInvoiceContainer } from '../../types';
import type { ApiWebDocument } from './bots';
import type { ApiChat } from './chats';
import type {
ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia,
ApiDocument,
ApiFormattedText,
ApiInvoice,
ApiMessageEntity,
ApiPaymentCredentials,
BoughtPaidMedia,
} from './messages';
import type { ApiStarsSubscriptionPricing } from './misc';
import type { StatisticsOverviewPercentage } from './statistics';
@ -34,19 +38,32 @@ export interface ApiPaymentFormRegular {
formId: string;
providerId: string;
nativeProvider?: string;
nativeParams: ApiPaymentFormNativeParams;
savedInfo?: ApiPaymentSavedInfo;
savedCredentials?: ApiPaymentCredentials[];
invoiceContainer: ApiInvoiceContainer;
nativeParams: ApiPaymentFormNativeParams;
invoice: ApiInvoice;
title: string;
description: string;
photo?: ApiWebDocument;
}
export interface ApiPaymentFormStars {
type: 'stars';
formId: string;
botId: string;
title: string;
description: string;
photo?: ApiWebDocument;
invoice: ApiInvoice;
}
export type ApiPaymentForm = ApiPaymentFormRegular | ApiPaymentFormStars;
export interface ApiPaymentFormStarGift {
type: 'stargift';
formId: string;
invoice: ApiInvoice;
}
export type ApiPaymentForm = ApiPaymentFormRegular | ApiPaymentFormStars | ApiPaymentFormStarGift;
export interface ApiPaymentFormNativeParams {
needCardholderName?: boolean;
@ -64,25 +81,25 @@ export interface ApiLabeledPrice {
export interface ApiReceiptStars {
type: 'stars';
peer: ApiStarsTransactionPeer;
date: number;
title?: string;
text?: string;
botId: string;
title: string;
description: string;
invoice: ApiInvoice;
photo?: ApiWebDocument;
media?: BoughtPaidMedia[];
currency: string;
totalAmount: number;
transactionId: string;
messageId?: number;
}
export interface ApiReceiptRegular {
type: 'regular';
botId: string;
providerId: string;
description: string;
title: string;
invoice: ApiInvoice;
photo?: ApiWebDocument;
text?: string;
title?: string;
currency: string;
prices: ApiLabeledPrice[];
info?: {
shippingAddress?: ApiShippingAddress;
phone?: string;
@ -90,6 +107,8 @@ export interface ApiReceiptRegular {
};
tipAmount: number;
totalAmount: number;
currency: string;
date: number;
credentialsTitle: string;
shippingPrices?: ApiLabeledPrice[];
shippingMethod?: string;
@ -133,6 +152,7 @@ export type ApiInputStorePaymentGiftcode = {
boostChannel?: ApiChat;
currency: string;
amount: number;
message?: ApiFormattedText;
};
export type ApiInputStorePaymentStarsTopup = {
@ -168,6 +188,28 @@ export type ApiInputStorePaymentStarsGiveaway = {
export type ApiInputStorePaymentPurpose = ApiInputStorePaymentGiveaway | ApiInputStorePaymentGiftcode |
ApiInputStorePaymentStarsTopup | ApiInputStorePaymentStarsGift | ApiInputStorePaymentStarsGiveaway;
export type ApiStarGift = {
isLimited?: true;
id: string;
stickerId: string;
stars: number;
availabilityRemains?: number;
availabilityTotal?: number;
starsToConvert: number;
};
export interface ApiUserStarGift {
isNameHidden?: boolean;
isUnsaved?: boolean;
fromId?: string;
date: number;
gift: ApiStarGift;
message?: ApiFormattedText;
messageId?: number;
starsToConvert?: number;
isConverted?: boolean; // Local field, used for Action Message
}
export interface ApiPremiumGiftCodeOption {
users: number;
months: number;
@ -302,7 +344,8 @@ export interface ApiStarsTransaction {
stars: number;
isRefund?: true;
isGift?: true;
isPrizeStars?: true;
starGift?: ApiStarGift;
giveawayPostId?: number;
isMyGift?: true; // Used only for outgoing star gift messages
isReaction?: true;
hasFailed?: true;

View File

@ -1,4 +1,4 @@
import type { ApiDraft } from '../../global/types';
import type { ApiDraft, TabState } from '../../global/types';
import type {
GroupCallConnectionData,
GroupCallConnectionState,
@ -20,7 +20,6 @@ import type {
} from './chats';
import type {
ApiFormattedText,
ApiInputInvoice,
ApiMediaExtendedPreview,
ApiMessage,
ApiPhoto,
@ -516,7 +515,14 @@ export type ApiUpdatePaymentVerificationNeeded = {
export type ApiUpdatePaymentStateCompleted = {
'@type': 'updatePaymentStateCompleted';
inputInvoice: ApiInputInvoice;
paymentState: TabState['payment'];
tabId: number;
};
export type ApiUpdateStarPaymentStateCompleted = {
'@type': 'updateStarPaymentStateCompleted';
paymentState: TabState['starsPayment'];
tabId: number;
};
export type ApiUpdatePrivacy = {
@ -779,7 +785,7 @@ export type ApiUpdate = (
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
ApiUpdateTwoFaError | ApiUpdatePasswordError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |

View File

@ -3,6 +3,7 @@ import type { ApiBotInfo } from './bots';
import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from './business';
import type { ApiPeerColor } from './chats';
import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiUserStarGift } from './payments';
export interface ApiUser {
id: string;
@ -58,6 +59,7 @@ export interface ApiUserFullInfo {
businessLocation?: ApiBusinessLocation;
businessWorkHours?: ApiBusinessWorkHours;
businessIntro?: ApiBusinessIntro;
starGiftCount?: number;
}
export type ApiFakeType = 'fake' | 'scam';
@ -81,6 +83,11 @@ export interface ApiUserCommonChats {
isFullyLoaded: boolean;
}
export interface ApiUserGifts {
gifts: ApiUserStarGift[];
nextOffset?: string;
}
export interface ApiUsername {
username: string;
isActive?: boolean;

View File

@ -33,12 +33,12 @@
"SetUrlInUse" = "Sorry, this link is already taken.";
"UsernameAvailable" = "{username} is available.";
"UsernameInUse" = "Sorry, this username is already taken.";
"CreateGroupError" = "Sorry, you can\'t create a group with these users because of their privacy settings.";
"CreateGroupError" = "Sorry, you can't create a group with these users because of their privacy settings.";
"PasscodeControllerErrorCurrent" = "invalid passcode";
"LimitReachedChatInFolders" = "Sorry, you can\'t add more than **{limit}** chats to a folder. You can increase this limit to **{limit2}** by subscribing to **Telegram Premium**.";
"LimitReachedChatInFolders" = "Sorry, you can't add more than **{limit}** chats to a folder. You can increase this limit to **{limit2}** by subscribing to **Telegram Premium**.";
"LimitReachedFileSize" = "The document cant be sent, because it is larger than **{limit}**. You can double this limit to **{limit2}** per document by subscribing to **Telegram Premium**.";
"LimitReachedFolders" = "You have reached the limit of **{limit}** folders. You can double the limit to **{limit2}** folders by subscribing to **Telegram Premium**.";
"LimitReachedPinDialogs" = "You can\'t pin more than {limit} chats to the top. Unpin some that are currently pinned or subscribe to **Telegram Premium** to double the limit to **{limit2}** chats.";
"LimitReachedPinDialogs" = "You can't pin more than {limit} chats to the top. Unpin some that are currently pinned or subscribe to **Telegram Premium** to double the limit to **{limit2}** chats.";
"LimitReachedPublicLinks" = "You have reserved too many public links. Try revoking the link from an older group or channel, or subscribe to **Telegram Premium** to double the limit to **{limit2}** public links.";
"LimitReachedCommunities" = "You are a member of **{limit}** groups and channels. Please leave some before joining a new one — or subscribe to **Telegram Premium** to double the limit to **{limit2}** groups and channels.";
"LimitReachedChatInFoldersLocked" = "Sorry, you can't add more than **{limit}** chats to a folder. Please create a new one. We are working to let you increase this limit in the future.";
@ -50,7 +50,7 @@
"LimitReachedChatInFoldersPremium" = "Sorry, you can't add more than **{limit}** chats to a folder. Please create a new one.";
"LimitReachedFileSizePremium" = "The document can't be sent, because it is larger than **{limit}**.";
"LimitReachedFoldersPremium" = "You have reached the limit of **{limit}** folders for this account.";
"LimitReachedPinDialogsPremium" = "Sorry, you can\'t pin more than {limit} chats to the top. Unpin some that are currently pinned.";
"LimitReachedPinDialogsPremium" = "Sorry, you can't pin more than {limit} chats to the top. Unpin some that are currently pinned.";
"LimitReachedPublicLinksPremium" = "You have reserved too many public links. Try revoking the link from an older group or channel.";
"LimitReachedCommunitiesPremium" = "You are a member of **{limit}** groups and channels. Please leave some before joining a new one.";
"PremiumPreviewLimits" = "Doubled Limits";
@ -116,12 +116,12 @@
"MegaPrivateLinkHelp" = "People can join your group by following this link. You can revoke the link at any time.";
"ChannelUsernameCreatePublicLinkHelp" = "If you set a public link, other people will be able to find and join your channel.\n\nYou can use az, 09 and underscores.\nMinimum length is 5 characters.";
"GroupUsernameCreatePublicLinkHelp" = "People can share this link with others and find your group using Telegram search.";
"UserRestrictionsNoSend" = "can\'t send messages";
"UserRestrictionsNoSend" = "can't send messages";
"UserRestrictionsNoSendMedia" = "no media";
"UserRestrictionsNoSendStickers" = "no stickers & GIFs";
"UserRestrictionsNoEmbedLinks" = "no embed links";
"UserRestrictionsNoSendPolls" = "no polls";
"UserRestrictionsNoChangeInfo" = "can\'t change Info";
"UserRestrictionsNoChangeInfo" = "can't change Info";
"UserRestrictionsInviteUsers" = "Add Users";
"UserRestrictionsPinMessages" = "Pin Messages";
"StatsMessageInteractionsTitle" = "INTERACTIONS";
@ -151,7 +151,7 @@
"ChannelStatsOverviewViewsPerPost" = "Views Per Post";
"ChannelStatsOverviewSharesPerPost" = "Shares Per Post";
"WrongNumber" = "Wrong number?";
"SentAppCode" = "We\'ve sent the code to the **Telegram** app on your other device.";
"SentAppCode" = "We've sent the code to the **Telegram** app on your other device.";
"LoginJustSentSms" = "We've sent you a code via SMS. Please enter it above.";
"Code" = "Code";
"LoginHeaderPassword" = "Enter Password";
@ -228,7 +228,7 @@
"Send" = "Send";
"SponsoredMessageInfo" = "What are sponsored\nmessages?";
"SponsoredMessageInfoDescription1" = "Unlike other apps, Telegram never uses your private data to target ads. Sponsored messages on Telegram are based solely on the topic of the public channels in which they are shown. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored messages.";
"SponsoredMessageInfoDescription2" = "Unlike other apps, Telegram doesn\'t track whether you tapped on a sponsored message and doesn\'t profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties cant spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.";
"SponsoredMessageInfoDescription2" = "Unlike other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties cant spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.";
"SponsoredMessageInfoDescription3" = "Telegram offers a free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at:";
"SponsoredMessageAlertLearnMoreUrl" = "https://ads.telegram.org";
"SponsoredMessageInfoDescription4" = "Sponsored Messages are currently in test mode. Once they are fully launched and allow Telegram to cover its basic costs, we will start sharing ad revenue with the owners of public channels in which sponsored messages are displayed.\n\nOnline ads should no longer be synonymous with abuse of user privacy. Let us redefine how a tech company should operate together.";
@ -499,10 +499,10 @@
"SettingsSensitiveTitle" = "Sensitive content";
"SettingsSensitiveDisableFiltering" = "Disable filtering";
"SettingsSensitiveAbout" = "Display sensitive media in public channels on all your Telegram devices.";
"BlockedUsersInfo" = "Blocked users can\'t send you messages or add you to groups. They will not see your profile pictures, online and last seen status.";
"BlockedUsersInfo" = "Blocked users can't send you messages or add you to groups. They will not see your profile pictures, online and last seen status.";
"NoBlocked" = "No blocked users yet";
"BlockContact" = "Block";
"CustomHelp" = "You won\'t see Last Seen or Online statuses for people with whom you don\'t share yours. Approximate times will be shown instead (recently, within a week, within a month).";
"CustomHelp" = "You won't see Last Seen or Online statuses for people with whom you don't share yours. Approximate times will be shown instead (recently, within a week, within a month).";
"PrivacyExceptions" = "Exceptions";
"AlwaysAllow" = "Always Allow";
"EditAdminAddUsers" = "Add Users";
@ -516,7 +516,7 @@
"TwoStepVerificationPasswordSetInfo" = "This password will be required when you log in on a new device in addition to the code you get in the SMS.";
"TwoStepVerificationPasswordReturnSettings" = "Return to Settings";
"YourEmailCode" = "Your Email Code";
"EnabledPasswordText" = "You have enabled Two-Step verification.\nYou\'ll need the password you set up here to log in to your Telegram account.";
"EnabledPasswordText" = "You have enabled Two-Step verification.\nYou'll need the password you set up here to log in to your Telegram account.";
"ChangePassword" = "Change Password";
"TurnPasswordOff" = "Disable Password";
"SetRecoveryEmail" = "Set Recovery Email";
@ -814,8 +814,8 @@
"ChannelVisibilityForwardingGroupInfo" = "Members will be able to copy, save and forward content from this group.";
"UserRemovedBy" = "Removed by {user}";
"Unblock" = "Unblock";
"NoBlockedChannel2" = "Users removed from the channel by admins can\'t rejoin via invite links.";
"NoBlockedGroup2" = "Users removed from the group by admins can\'t rejoin via invite links.";
"NoBlockedChannel2" = "Users removed from the channel by admins can't rejoin via invite links.";
"NoBlockedGroup2" = "Users removed from the group by admins can't rejoin via invite links.";
"ChannelEditAdminPermissionBanUsers" = "Ban Users";
"DiscussionUnlinkGroup" = "Unlink Group";
"DiscussionUnlinkChannel" = "Unlink Channel";
@ -982,7 +982,7 @@
"LimitReachedFavoriteStickersSubtitle" = "An older sticker was replaced with this one. You can **increase the limit** to {count} stickers.";
"StickerPackErrorNotFound" = "Sorry, this sticker set doesn't seem to exist.";
"ContactsPhoneNumberNotRegistred" = "The person with this phone number is not registered on Telegram yet.";
"VoipPeerIncompatible" = "**{user}**\'s app is using an incompatible protocol. They need to update their app before you can call them.";
"VoipPeerIncompatible" = "**{user}**'s app is using an incompatible protocol. They need to update their app before you can call them.";
"NoUsernameFound" = "Username not found.";
"HiddenName" = "Deleted Account";
"ChannelPersmissionDeniedSendMessagesForever" = "The admins of this group have restricted your ability to send messages.";
@ -1020,7 +1020,7 @@
"VoipMutedTapedForSpeak" = "You asked to speak";
"VoipMutedByAdmin" = "Muted by admin";
"VoipUnmute" = "Unmute";
"VoipTapToMute" = "You\'re live";
"VoipTapToMute" = "You're live";
"Weekday1" = "Mon";
"Weekday2" = "Tue";
"Weekday3" = "Wed";
@ -1280,21 +1280,84 @@
"ChannelEarnAbout" = "Telegram shares 50% of the revenue from ads displayed in your channel as rewards. {link}";
"AriaSearchOlderResult" = "Focus next result";
"AriaSearchNewerResult" = "Focus previous result";
"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}"
"StarsTransactionTOS" = "Review the {link} for Stars."
"StarsTransactionTOSLinkText" = "Terms of Service"
"StarsTransactionTOSLink" = "https://telegram.org/tos/stars"
"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram."
"SendPaidReaction" = "Send ⭐️{amount}"
"StarsReactionTerms" = "By sending Stars you agree to the {link}"
"StarsReactionLinkText" = "Terms of Service"
"StarsReactionLink" = "https://telegram.org/tos/stars"
"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}";
"StarsTransactionTOS" = "Review the {link} for Stars.";
"StarsTransactionTOSLinkText" = "Terms of Service";
"StarsTransactionTOSLink" = "https://telegram.org/tos/stars";
"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram.";
"GiftPremiumHeader" = "Gift Premium";
"GiftPremiumDescription" = "Give {user} access to exclusive features with Telegram Premium. {link}";
"GiftPremiumDescriptionLinkCaption" = "See Features >";
"GiftPremiumDescriptionLink" = "https://telegram.org/faq_premium";
"StarsGiftHeader" = "Send a Gift";
"StarGiftDescription" = "Give {user} gifts that can be kept on the profile or converted to Stars.";
"GiftLimited" = "limited";
"GiftDiscount" = "-{percent}%";
"GiftSoldCount" = "{count} sold";
"GiftLeftCount" = "{count} left";
"GiftSoldOut" = "sold out";
"GiftSoldOutInfo" = "Sorry, this gift is sold out.";
"GiftMessagePlaceholder" = "Enter Message (Optional)";
"GiftHideMyName" = "Hide My Name";
"GiftHideNameDescription" = "Hide my name and message from visitors to {profile}'s profile. {receiver} will still see your name and message.";
"GiftSend" = "Send a Gift for {amount}";
"GiftInfoSent" = "Sent Gift";
"GiftInfoReceived" = "Received Gift";
"GiftInfoTitle" = "Gift";
"GiftInfoDescription_one" = "You can keep this gift in your Profile or convert it to **{amount}** Star.";
"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.";
"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.";
"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";
"GiftInfoConvertDescription" = "Do you want to convert this gift from **{user}** to **{amount}**?\n\nThis action cannot be undone. This will permanently destroy the gift.";
"GiftInfoSaved" = "This gift is visible on your profile. {link}";
"GiftInfoSavedView" = "View >";
"GiftInfoHidden" = "This gift is hidden. Only you can see it.";
"StarsAmount" = "⭐️{amount}";
"StarsAmountText_one" = "{amount} Star";
"StarsAmountText_other" = "{amount} Stars";
"AllGiftsCategory" = "All gifts";
"LimitedGiftsCategory" = "Limited";
"PremiumGiftDescription" = "Premium";
"SendPaidReaction" = "Send ⭐️{amount}";
"StarsReactionTerms" = "By sending Stars you agree to the {link}";
"StarsReactionLinkText" = "Terms of Service";
"StarsReactionLink" = "https://telegram.org/tos/stars";
"MiniAppsMoreTabs_one" = "{botName} & {count} Other";
"MiniAppsMoreTabs_other" = "{botName} & {count} Others";
"PrizeCredits" = "Your prize is {count} Stars."
"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}"
"StarsSubscribeInfoLinkText" = "Terms of Service"
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars"
"StarsPerMonth" = "⭐️{amount}/month"
"PrizeCredits" = "Your prize is {count} Stars.";
"ActionStarGiftTitle" = "{user} sent you a Gift for {count} Stars";
"ActionStarGiftOutTitle" = "You have sent a gift for {count} Stars";
"ActionStarGiftOutDescription" = "{user} can display this gift on their page or convert it to {count} Stars.";
"ActionStarGiftDescription" = "Display this gift on your page or convert it to {count} Stars.";
"ActionStarGiftDisplaying" = "You kept this gift on your page.";
"GiftTo" = "Gift to";
"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}";
"StarsSubscribeInfoLinkText" = "Terms of Service";
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars";
"StarsPerMonth" = "⭐️{amount}/month";

View File

@ -17,9 +17,7 @@ export { default as BotTrustModal } from '../components/main/BotTrustModal';
export { default as AttachBotInstallModal } from '../components/modals/attachBotInstall/AttachBotInstallModal';
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
export { default as PremiumGiftModal } from '../components/main/premium/PremiumGiftModal';
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
export { default as PremiumGiftingPickerModal } from '../components/main/premium/PremiumGiftingPickerModal';
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
export { default as BoostModal } from '../components/modals/boost/BoostModal';

View File

@ -1,7 +1,10 @@
export { default as StarsGiftModal } from '../components/main/premium/StarsGiftModal';
export { default as StarsGiftModal } from '../components/modals/stars/gift/StarsGiftModal';
export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal';
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';
export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal';
export { default as StarsSubscriptionModal } from '../components/modals/stars/subscription/StarsSubscriptionModal';
export { default as PaidReactionModal } from '../components/modals/paidReaction/PaidReactionModal';
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';

View File

@ -0,0 +1,16 @@
.root {
font-size: 0.75rem;
line-height: 1;
border-radius: 1em;
padding: 0.25em 0.5em;
background-color: var(--accent-background-active-color);
color: var(--accent-color);
cursor: var(--custom-cursor, pointer);
filter: brightness(1);
transition: 150ms filter ease-in;
&:hover {
filter: brightness(1.1);
}
}

View File

@ -0,0 +1,25 @@
import React from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import styles from './BadgeButton.module.scss';
type OwnProps = {
children: React.ReactNode;
className?: string;
onClick?: NoneToVoidFunction;
};
const BadgeButton = ({
children,
className,
onClick,
}: OwnProps) => {
return (
<div className={buildClassName(styles.root, className)} onClick={onClick}>
{children}
</div>
);
};
export default BadgeButton;

View File

@ -129,3 +129,14 @@
.fullProgress {
border-radius: 0.625rem;
}
.primary {
.progress {
background-image: none;
background-color: var(--color-primary);
}
.floating-badge {
background-color: var(--color-primary);
}
}

View File

@ -21,15 +21,17 @@ type OwnProps = {
floatingBadgeIcon?: IconName;
floatingBadgeText?: string;
progress?: number;
isPrimary?: boolean;
className?: string;
};
const LimitPreview: FC<OwnProps> = ({
const PremiumProgress: FC<OwnProps> = ({
leftText,
rightText,
floatingBadgeText,
floatingBadgeIcon,
progress,
isPrimary,
className,
}) => {
const lang = useOldLang();
@ -78,6 +80,7 @@ const LimitPreview: FC<OwnProps> = ({
className={buildClassName(
styles.root,
hasFloatingBadge && styles.withBadge,
isPrimary && styles.primary,
className,
)}
style={buildStyle(
@ -123,4 +126,4 @@ const LimitPreview: FC<OwnProps> = ({
);
};
export default memo(LimitPreview);
export default memo(PremiumProgress);

View File

@ -1,8 +1,6 @@
.root {
position: absolute;
width: 100%;
height: 100%;
z-index: -1;
inset: 0;
line-height: 1;
pointer-events: none;
}
@ -16,7 +14,7 @@
overflow: hidden;
}
.reaction {
.button {
font-size: 0.5rem;
}

View File

@ -5,15 +5,15 @@ import buildStyle from '../../util/buildStyle';
import styles from './Sparkles.module.scss';
type ReactionParameters = {
preset: 'reaction';
type ButtonParameters = {
preset: 'button';
};
type ProgressParameters = {
preset: 'progress';
};
type PresetParameters = ReactionParameters | ProgressParameters;
type PresetParameters = ButtonParameters | ProgressParameters;
type OwnProps = {
className?: string;
@ -23,7 +23,7 @@ const SYMBOL = '✦';
const ANIMATION_DURATION = 5;
// Values are in percents
const REACTION_POSITIONS = [{
const BUTTON_POSITIONS = [{
x: 20,
y: 0,
size: 100,
@ -85,10 +85,10 @@ const Sparkles = ({
className,
...presetSettings
}: OwnProps) => {
if (presetSettings.preset === 'reaction') {
if (presetSettings.preset === 'button') {
return (
<div className={buildClassName(styles.root, styles.reaction, className)}>
{REACTION_POSITIONS.map((position) => {
<div className={buildClassName(styles.root, styles.button, className)}>
{BUTTON_POSITIONS.map((position) => {
const shiftX = Math.cos(Math.atan2(-50 + position.y, -50 + position.x)) * 100;
const shiftY = Math.sin(Math.atan2(-50 + position.y, -50 + position.x)) * 100;
return (

View File

@ -0,0 +1,18 @@
.root {
position: absolute;
height: 3.5rem;
width: 3.5rem;
top: -0.125rem;
right: -0.125rem;
}
.text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) translate(6px, -6px) rotate(45deg);
font-size: 0.625rem;
color: var(--color-white);
white-space: nowrap;
}

View File

@ -0,0 +1,48 @@
import React, { memo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import useUniqueId from '../../../hooks/useUniqueId';
import styles from './GiftRibbon.module.scss';
const COLORS = {
red: ['#FF5B54', '#ED1C26'],
blue: ['#6ED2FF', '#34A4FC'],
} as const;
type ColorKey = keyof typeof COLORS;
const COLOR_KEYS = new Set(Object.keys(COLORS) as ColorKey[]);
type OwnProps = {
color: ColorKey | string;
text: string;
className?: string;
};
const GiftRibbon = ({ text, color, className }: OwnProps) => {
const randomId = useUniqueId();
const validSvgRandomId = `svg-${randomId}`; // ID must start with a letter
const colorKey = COLOR_KEYS.has(color as ColorKey) ? color as ColorKey : undefined;
const startColor = colorKey ? COLORS[colorKey][0] : color;
const endColor = colorKey ? COLORS[colorKey][1] : color;
return (
<div className={buildClassName(styles.root, className)}>
<svg className={styles.ribbon} width="56" height="56" viewBox="0 0 56 56" fill="none">
<path d="M52.4851 26.4853L29.5145 3.51472C27.2641 1.26428 24.2119 0 21.0293 0H2.82824C1.04643 0 0.154103 2.15429 1.41403 3.41422L52.5856 54.5858C53.8455 55.8457 55.9998 54.9534 55.9998 53.1716V34.9706C55.9998 31.788 54.7355 28.7357 52.4851 26.4853Z" fill={`url(#${validSvgRandomId})`} />
<defs>
<linearGradient id={validSvgRandomId} x1="27.9998" y1="1" x2="27.9998" y2="55" gradientUnits="userSpaceOnUse">
<stop stop-color={startColor} />
<stop offset="1" stop-color={endColor} />
</linearGradient>
</defs>
</svg>
<div className={styles.text}>{text}</div>
</div>
);
};
export default memo(GiftRibbon);

View File

@ -0,0 +1,62 @@
.root {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 0;
padding: 0.625rem;
padding-top: 0.875rem;
border-radius: 0.625rem;
background-color: var(--color-background-secondary);
position: relative;
cursor: var(--custom-cursor, pointer);
&::before {
content: "";
position: absolute;
inset: 0;
opacity: 0;
border-radius: 0.625rem;
background-color: var(--color-hover-overlay);
pointer-events: none;
}
&:hover::before {
opacity: 1;
}
}
.avatar {
position: absolute;
top: 0.25rem;
left: 0.25rem;
}
.stars {
display: flex;
align-items: center;
gap: 0.125rem;
color: #E88011;
font-weight: 500;
}
.hiddenGift {
display: grid;
place-items: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: var(--color-light-shadow);
color: white;
font-size: 1.25rem;
backdrop-filter: blur(0.5rem);
}

View File

@ -0,0 +1,89 @@
import React, { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiSticker, ApiUser, ApiUserStarGift } from '../../../api/types';
import { STARS_CURRENCY_CODE } from '../../../config';
import { selectUser } from '../../../global/selectors';
import { formatCurrency } from '../../../util/formatCurrency';
import { CUSTOM_PEER_HIDDEN } from '../../../util/objects/customPeer';
import { formatIntegerCompact } from '../../../util/textFormat';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconFromSticker from '../AnimatedIconFromSticker';
import Avatar from '../Avatar';
import Icon from '../icons/Icon';
import GiftRibbon from './GiftRibbon';
import styles from './UserGift.module.scss';
type OwnProps = {
userId: string;
gift: ApiUserStarGift;
};
type StateProps = {
fromPeer?: ApiUser;
sticker?: ApiSticker;
};
const GIFT_STICKER_SIZE = 90;
const UserGift = ({
userId, gift, fromPeer, sticker,
}: OwnProps & StateProps) => {
const { openGiftInfoModal } = getActions();
const oldLang = useOldLang();
const handleClick = useLastCallback(() => {
openGiftInfoModal({
userId,
gift,
});
});
const avatarPeer = (gift.isNameHidden || !fromPeer) ? CUSTOM_PEER_HIDDEN : fromPeer;
if (!sticker) return undefined;
return (
<div className={styles.root} onClick={handleClick}>
<Avatar className={styles.avatar} peer={avatarPeer} size="micro" />
<AnimatedIconFromSticker
sticker={sticker}
noLoop
nonInteractive
size={GIFT_STICKER_SIZE}
/>
{gift.isUnsaved && (
<div className={styles.hiddenGift}>
<Icon name="eye-closed-outline" />
</div>
)}
<div className={styles.stars}>
{formatCurrency(gift.gift.stars, STARS_CURRENCY_CODE)}
</div>
{gift.gift.availabilityTotal && (
<GiftRibbon
color="blue"
text={oldLang('Gift2Limited1OfRibbon', formatIntegerCompact(gift.gift.availabilityTotal))}
/>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { gift }): StateProps => {
const sticker = global.stickers.starGifts.stickers[gift.gift.stickerId];
const fromPeer = gift.fromId ? selectUser(global, gift.fromId) : undefined;
return {
sticker,
fromPeer,
};
},
)(UserGift));

View File

@ -36,7 +36,7 @@ const MAX_LENGTH = 32;
const NBSP = '\u00A0';
export function renderActionMessageText(
lang: LangFn,
oldLang: LangFn,
message: ApiMessage,
actionOriginUser?: ApiUser,
actionOriginChat?: ApiChat,
@ -49,23 +49,25 @@ export function renderActionMessageText(
observeIntersectionForPlaying?: ObserveFn,
) {
if (isExpiredMessage(message)) {
return getExpiredMessageDescription(lang, message);
return getExpiredMessageDescription(oldLang, message);
}
if (!message.content.action) {
if (!message.content?.action) {
return [];
}
const {
text, translationValues, amount, currency, call, score, topicEmojiIconId, giftCryptoInfo, pluralValue,
} = message.content.action;
const content: TextPart[] = [];
const noLinks = options.asPlainText || options.isEmbedded;
const content: TextPart[] = [];
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
? 'Message.PinnedGenericMessage'
: text;
let unprocessed = lang(
let unprocessed = oldLang(
translationKey, translationValues?.length ? translationValues : undefined, undefined, pluralValue,
);
if (translationKey.includes('ScoredInGame')) { // Translation hack for games
@ -116,10 +118,10 @@ export function renderActionMessageText(
'%action_origin%',
actionOriginUser ? (
actionOriginUser.id === SERVICE_NOTIFICATIONS_USER_ID
? lang('StarsTransactionUnknown')
? oldLang('StarsTransactionUnknown')
: renderUserContent(actionOriginUser, noLinks) || NBSP
) : actionOriginChat ? (
renderChatContent(lang, actionOriginChat, noLinks) || NBSP
renderChatContent(oldLang, actionOriginChat, noLinks) || NBSP
) : 'User',
'',
);
@ -131,7 +133,7 @@ export function renderActionMessageText(
processed = processPlaceholder(
unprocessed,
'%payment_amount%',
formatCurrencyAsString(amount!, currency!, lang.code),
formatCurrencyAsString(amount!, currency!, oldLang.code),
);
unprocessed = processed.pop() as string;
content.push(...processed);
@ -166,11 +168,11 @@ export function renderActionMessageText(
}
if (unprocessed.includes('%gift_payment_amount%')) {
const price = formatCurrencyAsString(amount!, currency!, lang.code);
const price = formatCurrencyAsString(amount!, currency!, oldLang.code);
let priceText = price;
if (giftCryptoInfo) {
const cryptoPrice = formatCurrencyAsString(giftCryptoInfo.amount, giftCryptoInfo.currency, lang.code);
const cryptoPrice = formatCurrencyAsString(giftCryptoInfo.amount, giftCryptoInfo.currency, oldLang.code);
priceText = `${cryptoPrice} (${price})`;
}
@ -220,7 +222,7 @@ export function renderActionMessageText(
'%message%',
targetMessage
? renderMessageContent(
lang, targetMessage, options, observeIntersectionForLoading, observeIntersectionForPlaying,
oldLang, targetMessage, options, observeIntersectionForLoading, observeIntersectionForPlaying,
)
: 'a message',
);

View File

@ -28,7 +28,7 @@ const PickerModal = ({
...modalProps
}: OwnProps) => {
const lang = useOldLang();
const hasOnClickHandler = Boolean(onConfirm || modalProps.onClose);
const hasButton = Boolean(confirmButtonText || onConfirm);
return (
<Modal
@ -44,7 +44,7 @@ const PickerModal = ({
headerClassName={buildClassName(styles.header, modalProps.headerClassName)}
>
{modalProps.children}
{hasOnClickHandler && (
{hasButton && (
<div className={styles.buttonWrapper}>
<Button
withPremiumGradient={withPremiumGradient}

View File

@ -53,7 +53,7 @@ const UserBirthday = ({
animatedEmojiEffects,
isInSettings,
}: OwnProps & StateProps) => {
const { openPremiumGiftModal, requestConfetti } = getActions();
const { openGiftModal, requestConfetti } = getActions();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const animationPlayedRef = useRef(false);
@ -144,15 +144,15 @@ const UserBirthday = ({
const canGiftPremium = isToday && !user.isPremium && !user.isSelf && !isPremiumPurchaseBlocked;
const handleOpenPremiumGiftModal = useLastCallback(() => {
openPremiumGiftModal({ forUserIds: [user.id] });
const handleOpenGiftModal = useLastCallback(() => {
openGiftModal({ forUserId: user.id });
});
const handleClick = useLastCallback(() => {
if (!isToday) return;
if (canGiftPremium && animationPlayedRef.current) {
handleOpenPremiumGiftModal();
handleOpenGiftModal();
return;
}
@ -173,7 +173,7 @@ const UserBirthday = ({
ripple={!isStatic}
onClick={handleClick}
isStatic={isStatic}
onSecondaryIconClick={handleOpenPremiumGiftModal}
onSecondaryIconClick={handleOpenGiftModal}
>
<div className="title" dir={lang.isRtl ? 'rtl' : undefined}>
{renderText(lang(valueKey, [formattedDate, age], undefined, age))}

View File

@ -81,7 +81,7 @@ export default function useChatListEntry({
orderDiff: number;
withInterfaceAnimations?: boolean;
}) {
const lang = useOldLang();
const oldLang = useOldLang();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
@ -120,8 +120,8 @@ export default function useChatListEntry({
if (canDisplayDraft) {
return (
<p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
<span className="draft">{lang('Draft')}</span>
<p className="last-message" dir={oldLang.isRtl ? 'auto' : 'ltr'}>
<span className="draft">{oldLang('Draft')}</span>
{renderTextWithEntities({
text: draft.text?.text || '',
entities: draft.text?.entities,
@ -138,8 +138,8 @@ export default function useChatListEntry({
if (isExpiredMessage(lastMessage)) {
return (
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
{getExpiredMessageDescription(lang, lastMessage)}
<p className="last-message shared-canvas-container" dir={oldLang.isRtl ? 'auto' : 'ltr'}>
{getExpiredMessageDescription(oldLang, lastMessage)}
</p>
);
}
@ -148,9 +148,9 @@ export default function useChatListEntry({
const isChat = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId);
return (
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
<p className="last-message shared-canvas-container" dir={oldLang.isRtl ? 'auto' : 'ltr'}>
{renderActionMessageText(
lang,
oldLang,
lastMessage,
!isChat ? lastMessageSender as ApiUser : undefined,
isChat ? chat : undefined,
@ -166,10 +166,10 @@ export default function useChatListEntry({
);
}
const senderName = getMessageSenderName(lang, chatId, lastMessageSender);
const senderName = getMessageSenderName(oldLang, chatId, lastMessageSender);
return (
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
<p className="last-message shared-canvas-container" dir={oldLang.isRtl ? 'auto' : 'ltr'}>
{senderName && (
<>
<span className="sender-name">{renderText(senderName)}</span>
@ -183,7 +183,7 @@ export default function useChatListEntry({
);
}, [
actionTargetChatId, actionTargetMessage, actionTargetUsers, chat, chatId, draft, isAction,
isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic, mediaBlobUrl, mediaThumbnail,
isRoundVideo, isTopic, oldLang, lastMessage, lastMessageSender, lastMessageTopic, mediaBlobUrl, mediaThumbnail,
observeIntersection, typingStatus, isSavedDialog, isPreview,
]);

View File

@ -53,7 +53,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
openPremiumModal,
openSupportChat,
openUrl,
openPremiumGiftingModal,
openGiftRecipientPicker,
openStarsBalanceModal,
} = getActions();
@ -197,9 +197,9 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
icon="gift"
narrow
// eslint-disable-next-line react/jsx-no-bind
onClick={() => openPremiumGiftingModal()}
onClick={() => openGiftRecipientPicker()}
>
{oldLang('GiftPremiumGifting')}
{oldLang('SendAGift')}
</ListItem>
)}
</div>

View File

@ -83,7 +83,6 @@ import NewContactModal from './NewContactModal.async';
import Notifications from './Notifications.async';
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
import GiveawayModal from './premium/GiveawayModal.async';
import PremiumGiftingPickerModal from './premium/PremiumGiftingPickerModal.async';
import PremiumMainModal from './premium/PremiumMainModal.async';
import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async';
import SafeLinkModal from './SafeLinkModal.async';
@ -137,7 +136,6 @@ type StateProps = {
isReactionPickerOpen: boolean;
isGiveawayModalOpen?: boolean;
isDeleteMessageModalOpen?: boolean;
isPremiumGiftingPickerModal?: boolean;
isStarsGiftingPickerModal?: boolean;
isCurrentUserPremium?: boolean;
noRightColumnAnimation?: boolean;
@ -188,7 +186,6 @@ const Main = ({
isPremiumModalOpen,
isGiveawayModalOpen,
isDeleteMessageModalOpen,
isPremiumGiftingPickerModal,
isStarsGiftingPickerModal,
isPaymentModalOpen,
isReceiptModalOpen,
@ -215,6 +212,7 @@ const Main = ({
loadAvailableReactions,
loadStickerSets,
loadPremiumGifts,
loadStarGifts,
loadDefaultTopicIcons,
loadAddedStickers,
loadFavoriteStickers,
@ -327,6 +325,7 @@ const Main = ({
loadQuickReplies();
loadStarStatus();
loadPremiumGifts();
loadStarGifts();
loadAvailableEffects();
loadBirthdayNumbersStickers();
loadRestrictedEmojiStickers();
@ -579,7 +578,6 @@ const Main = ({
<MessageListHistoryHandler />
<PremiumMainModal isOpen={isPremiumModalOpen} />
<GiveawayModal isOpen={isGiveawayModalOpen} />
<PremiumGiftingPickerModal isOpen={isPremiumGiftingPickerModal} />
<StarsGiftingPickerModal isOpen={isStarsGiftingPickerModal} />
<PremiumLimitReachedModal limit={limitReached} />
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
@ -621,8 +619,7 @@ export default memo(withGlobal<OwnProps>(
premiumModal,
giveawayModal,
deleteMessageModal,
giftingModal,
starsGiftingModal,
starsGiftingPickerModal,
isMasterTab,
payment,
limitReachedModal,
@ -678,8 +675,7 @@ export default memo(withGlobal<OwnProps>(
isPremiumModalOpen: premiumModal?.isOpen,
isGiveawayModalOpen: giveawayModal?.isOpen,
isDeleteMessageModalOpen: Boolean(deleteMessageModal),
isPremiumGiftingPickerModal: giftingModal?.isOpen,
isStarsGiftingPickerModal: starsGiftingModal?.isOpen,
isStarsGiftingPickerModal: starsGiftingPickerModal?.isOpen,
limitReached: limitReachedModal?.limit,
isPaymentModalOpen: payment.isPaymentModalOpen,
isReceiptModalOpen: Boolean(payment.receipt),

View File

@ -80,7 +80,7 @@ type StateProps = {
prepaidGiveaway?: ApiTypePrepaidGiveaway;
countrySelectionLimit: number | undefined;
isChannel?: boolean;
isStarsGiftsEnabled?: boolean;
isStarsGiftEnabled?: boolean;
starsGiftOptions?: ApiStarGiveawayOption[] | undefined;
};
@ -120,7 +120,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
prepaidGiveaway,
countrySelectionLimit = GIVEAWAY_MAX_ADDITIONAL_COUNTRIES,
userSelectionLimit = GIVEAWAY_MAX_ADDITIONAL_USERS,
isStarsGiftsEnabled,
isStarsGiftEnabled,
starsGiftOptions,
}) => {
// eslint-disable-next-line no-null/no-null
@ -149,7 +149,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
},
}];
if (isStarsGiftsEnabled) {
if (isStarsGiftEnabled) {
TYPE_OPTIONS.push({
name: 'TelegramStars',
text: 'BoostingWinnersRandomly',
@ -914,7 +914,7 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
selectedMemberList: giveawayModal?.selectedMemberIds,
selectedChannelList: giveawayModal?.selectedChannelIds,
giveawayBoostPerPremiumLimit: global.appConfig?.giveawayBoostsPerPremium,
isStarsGiftsEnabled: global.appConfig?.isStarsGiftsEnabled,
isStarsGiftEnabled: global.appConfig?.isStarsGiftEnabled,
userSelectionLimit: global.appConfig?.giveawayAddPeersMax,
countrySelectionLimit: global.appConfig?.giveawayCountriesMax,
countryList: global.countryList.general,

View File

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

View File

@ -1,283 +0,0 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useRef,
useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type {
ApiPremiumGiftCodeOption,
} from '../../../api/types';
import { BOOST_PER_SENT_GIFT } from '../../../config';
import { getUserFullName } from '../../../global/helpers';
import {
selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatCurrency } from '../../../util/formatCurrency';
import renderText from '../../common/helpers/renderText';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import AvatarList from '../../common/AvatarList';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import Link from '../../ui/Link';
import Modal from '../../ui/Modal';
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
import styles from './PremiumGiftModal.module.scss';
export type OwnProps = {
isOpen?: boolean;
};
type StateProps = {
isCompleted?: boolean;
gifts?: ApiPremiumGiftCodeOption[] | undefined;
forUserIds?: string[];
boostPerSentGift?: number;
};
const PremiumGiftModal: FC<OwnProps & StateProps> = ({
isOpen,
isCompleted,
gifts,
boostPerSentGift = BOOST_PER_SENT_GIFT,
forUserIds,
}) => {
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
const {
openPremiumModal, closePremiumGiftModal, openInvoice, requestConfetti,
} = getActions();
const oldLang = useOldLang();
const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>();
const selectedUserQuantity = forUserIds && forUserIds.length * boostPerSentGift;
useEffect(() => {
if (forUserIds?.length) {
setSelectedMonthOption(gifts?.[0].months);
}
}, [gifts, forUserIds]);
const giftingUserList = useMemo(() => {
const usersById = getGlobal().users.byId;
return forUserIds?.map((userId) => usersById[userId]).filter(Boolean);
}, [forUserIds]);
const selectedGift = useMemo(() => {
return gifts?.find((gift) => gift.months === selectedMonthOption && gift.users === forUserIds?.length);
}, [gifts, selectedMonthOption, forUserIds?.length]);
const filteredGifts = useMemo(() => {
return gifts?.filter((gift) => gift.users
=== forUserIds?.length);
}, [gifts, forUserIds?.length]);
const fullMonthlyGiftAmount = useMemo(() => {
if (!filteredGifts?.length) {
return undefined;
}
const basicGift = filteredGifts.reduce((acc, gift) => {
return gift.amount < acc.amount ? gift : acc;
});
return Math.floor(basicGift.amount / basicGift.months);
}, [filteredGifts]);
const handleSubmit = useLastCallback(() => {
if (!selectedGift) {
return;
}
openInvoice({
type: 'giftcode',
userIds: forUserIds!,
currency: selectedGift!.currency,
amount: selectedGift!.amount,
option: selectedGift!,
});
});
const handlePremiumClick = useLastCallback(() => {
openPremiumModal();
});
const showConfetti = useLastCallback(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
const {
top, left, width, height,
} = dialog.querySelector('.modal-content')!.getBoundingClientRect();
requestConfetti({
top,
left,
width,
height,
withStars: true,
});
}
});
useEffect(() => {
if (isCompleted) {
showConfetti();
}
}, [isCompleted, showConfetti]);
const userNameList = useMemo(() => {
const usersById = getGlobal().users.byId;
return forUserIds?.map((userId) => getUserFullName(usersById[userId])).join(', ');
}, [forUserIds]);
function renderGiftTitle() {
if (isCompleted) {
return renderText(oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle',
[userNameList, selectedGift?.months]), ['simple_markdown']);
}
return oldLang('GiftTelegramPremiumTitle');
}
function renderGiftText() {
if (isCompleted) {
return renderText(oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', userNameList),
['simple_markdown']);
}
return renderText(oldLang('GiftPremiumUsersGiveAccessManyZero', userNameList), ['simple_markdown']);
}
function renderPremiumFeaturesLink() {
const info = oldLang('GiftPremiumListFeaturesAndTerms');
// Translation hack for rendering component inside string
const parts = info.match(/([^*]*)\*([^*]+)\*(.*)/);
if (!parts || parts.length < 4) {
return undefined;
}
return (
<p className={buildClassName(styles.premiumFeatures, styles.center)}>
{parts[1]}
<Link isPrimary onClick={handlePremiumClick}>{parts[2]}</Link>
{parts[3]}
</p>
);
}
function renderBoostsPluralText() {
const giftParts = renderText(oldLang('GiftPremiumWillReceiveBoostsPlural',
selectedUserQuantity), ['simple_markdown']);
return giftParts.map((part) => {
if (typeof part === 'string') {
return part.split(/(⚡)/g).map((subpart) => {
if (subpart === '⚡') {
return <Icon name="boost" className={styles.boostIcon} />;
}
return subpart;
});
}
return part;
});
}
function renderSubscriptionGiftOptions() {
return (
<div className={styles.subscriptionOptions}>
{filteredGifts?.map((gift) => {
return (
<PremiumSubscriptionOption
className={styles.subscriptionOption}
key={gift.months}
option={gift}
fullMonthlyAmount={fullMonthlyGiftAmount}
checked={gift.months === selectedMonthOption}
onChange={setSelectedMonthOption}
/>
);
})}
</div>
);
}
return (
<Modal
dialogRef={dialogRef}
onClose={closePremiumGiftModal}
isOpen={isOpen}
contentClassName={styles.content}
className={buildClassName(styles.modalDialog, styles.root)}
>
<div className={buildClassName(styles.main, 'custom-scroll')}>
<Button
round
size="smaller"
className={styles.closeButton}
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => closePremiumGiftModal()}
ariaLabel={oldLang('Close')}
>
<i className="icon icon-close" />
</Button>
<div className={styles.avatars}>
<AvatarList
size="large"
peers={giftingUserList}
/>
</div>
<h2 className={buildClassName(styles.headerText, styles.center)}>
{renderGiftTitle()}
</h2>
<p className={buildClassName(styles.description, styles.center)}>
{renderGiftText()}
</p>
{!isCompleted && (
<>
<p className={styles.description}>
{renderText(renderBoostsPluralText(), ['simple_markdown', 'emoji'])}
</p>
<div className={styles.giftSection}>
{renderSubscriptionGiftOptions()}
</div>
</>
)}
{renderPremiumFeaturesLink()}
</div>
{!isCompleted && (
<div className={styles.footer}>
<Button withPremiumGradient isShiny disabled={!selectedGift} onClick={handleSubmit}>
{oldLang(
'GiftSubscriptionFor', selectedGift
&& formatCurrency(selectedGift!.amount, selectedGift.currency, oldLang.code),
)}
</Button>
</div>
)}
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
gifts, forUserIds, isCompleted,
} = selectTabState(global).giftModal || {};
return {
isCompleted,
gifts,
boostPerSentGift: global.appConfig?.boostsPerSentGift,
forUserIds,
};
})(PremiumGiftModal));

View File

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

View File

@ -1,80 +0,0 @@
.root :global(.modal-content) {
padding: 0;
}
.root :global(.modal-dialog) {
max-width: 55vh;
}
.root :global(.modal-dialog), .root :global(.modal-content) {
overflow: hidden;
}
.main {
height: 90vh;
}
.filter {
padding: 0.375rem 1rem 0.25rem 0.75rem;
margin-bottom: 0.625rem;
background-color: var(--color-background);
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
border-bottom: 0.625rem solid var(--color-background-secondary);
display: flex;
flex-flow: row wrap;
align-items: center;
flex-shrink: 0;
overflow-y: auto;
max-height: 20rem;
}
.title {
margin: 0;
}
.buttons {
width: 100%;
background: var(--color-background);
position: absolute;
bottom: 0;
z-index: 1;
padding: 0.75rem;
}
.picker {
height: 75vh;
}
.avatars {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
}
.center {
text-align: center;
}
.description,
.premiumFeatures {
text-align: center;
margin: 0 auto 2rem;
max-width: 25rem;
}
.premiumFeatures {
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
.options {
margin-bottom: 2.5rem;
}
.button {
height: 3rem;
font-weight: 600;
}

View File

@ -1,117 +0,0 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import { GIVEAWAY_MAX_ADDITIONAL_CHANNELS } from '../../../config';
import {
filterUsersByName, isUserBot,
} from '../../../global/helpers';
import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import PeerPicker from '../../common/pickers/PeerPicker';
import PickerModal from '../../common/pickers/PickerModal';
import styles from './PremiumGiftingPickerModal.module.scss';
export type OwnProps = {
isOpen?: boolean;
};
interface StateProps {
currentUserId?: string;
userSelectionLimit?: number;
userIds?: string[];
}
const PremiumGiftingPickerModal: FC<OwnProps & StateProps> = ({
isOpen,
currentUserId,
userSelectionLimit = GIVEAWAY_MAX_ADDITIONAL_CHANNELS,
userIds,
}) => {
const { closePremiumGiftingModal, openPremiumGiftModal, showNotification } = getActions();
const oldLang = useOldLang();
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState<string>('');
const displayedUserIds = useMemo(() => {
const usersById = getGlobal().users.byId;
const filteredContactIds = userIds ? filterUsersByName(userIds, usersById, searchQuery) : [];
return sortChatIds(unique(filteredContactIds).filter((userId) => {
const user = usersById[userId];
if (!user) {
return true;
}
return !isUserBot(user) && userId !== currentUserId;
}));
}, [currentUserId, searchQuery, userIds]);
const handleSendIdList = useLastCallback(() => {
if (selectedUserIds?.length) {
openPremiumGiftModal({ forUserIds: selectedUserIds });
closePremiumGiftingModal();
}
});
const handleSelectedUserIdsChange = useLastCallback((newSelectedIds: string[]) => {
if (newSelectedIds.length > userSelectionLimit) {
showNotification({
message: oldLang('BoostingSelectUpToWarningUsers', userSelectionLimit),
});
return;
}
setSelectedUserIds(newSelectedIds);
});
return (
<PickerModal
className={styles.root}
isOpen={isOpen}
onClose={closePremiumGiftingModal}
title={oldLang('GiftTelegramPremiumTitle')}
hasCloseButton
shouldAdaptToSearch
withFixedHeight
confirmButtonText={oldLang('Continue')}
onConfirm={handleSendIdList}
onEnter={handleSendIdList}
withPremiumGradient
isConfirmDisabled={!selectedUserIds?.length}
>
<PeerPicker
className={styles.picker}
itemIds={displayedUserIds}
selectedIds={selectedUserIds}
filterValue={searchQuery}
filterPlaceholder={oldLang('Search')}
onSelectedIdsChange={handleSelectedUserIdsChange}
onFilterChange={setSearchQuery}
isSearchable
withDefaultPadding
withStatus
allowMultiple
itemInputType="checkbox"
/>
</PickerModal>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const { currentUserId } = global;
return {
currentUserId,
userIds: global.contactList?.userIds,
userSelectionLimit: global.appConfig?.giveawayAddPeersMax,
};
})(PremiumGiftingPickerModal));

View File

@ -38,7 +38,7 @@ const StarsGiftingPickerModal: FC<OwnProps & StateProps> = ({
archivedListIds,
userIds,
}) => {
const { closeStarsGiftingModal, openStarsGiftModal } = getActions();
const { closeStarsGiftingPickerModal, openStarsGiftModal } = getActions();
const oldLang = useOldLang();
@ -70,6 +70,7 @@ const StarsGiftingPickerModal: FC<OwnProps & StateProps> = ({
const handleSelectedUserIdsChange = useLastCallback((newSelectedId?: string) => {
if (newSelectedId?.length) {
openStarsGiftModal({ forUserId: newSelectedId });
closeStarsGiftingPickerModal();
}
});
@ -77,13 +78,13 @@ const StarsGiftingPickerModal: FC<OwnProps & StateProps> = ({
<PickerModal
className={styles.root}
isOpen={isOpen}
onClose={closeStarsGiftingModal}
onClose={closeStarsGiftingPickerModal}
title={oldLang('GiftStarsTitle')}
hasCloseButton
shouldAdaptToSearch
withFixedHeight
confirmButtonText={oldLang('Continue')}
onEnter={closeStarsGiftingModal}
onEnter={closeStarsGiftingPickerModal}
>
<PeerPicker
className={styles.picker}

View File

@ -21,14 +21,17 @@ import {
selectGiftStickerForDuration,
selectGiftStickerForStars,
selectIsMessageFocused,
selectStarGiftSticker,
selectTabState,
selectTheme,
selectTopicFromMessage,
selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { formatInteger } from '../../util/textFormat';
import { formatInteger, formatIntegerCompact } from '../../util/textFormat';
import { renderActionMessageText } from '../common/helpers/renderActionMessageText';
import renderText from '../common/helpers/renderText';
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
@ -41,6 +44,9 @@ import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated
import useFocusMessage from './message/hooks/useFocusMessage';
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
import Avatar from '../common/Avatar';
import GiftRibbon from '../common/gift/GiftRibbon';
import Sparkles from '../common/Sparkles';
import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar';
import ContextMenuContainer from './message/ContextMenuContainer.async';
import SimilarChannels from './message/SimilarChannels';
@ -74,10 +80,13 @@ type StateProps = {
noFocusHighlight?: boolean;
premiumGiftSticker?: ApiSticker;
starGiftSticker?: ApiSticker;
starsGiftSticker?: ApiSticker;
canPlayAnimatedEmojis?: boolean;
patternColor?: string;
};
const APPEARANCE_DELAY = 10;
const STAR_GIFT_STICKER_SIZE = 120;
const ActionMessage: FC<OwnProps & StateProps> = ({
message,
@ -96,10 +105,12 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
noFocusHighlight,
premiumGiftSticker,
starGiftSticker,
starsGiftSticker,
isInsideTopic,
topic,
memoFirstUnreadIdRef,
canPlayAnimatedEmojis,
patternColor,
observeIntersectionForReading,
observeIntersectionForLoading,
observeIntersectionForPlaying,
@ -110,7 +121,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
requestConfetti,
checkGiftCode,
getReceipt,
openStarsTransactionFromGift,
openGiftInfoModalFromMessage,
openPrizeStarsTransactionFromGiveaway,
} = getActions();
@ -141,6 +152,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
const isJoinedMessage = isJoinedChannelMessage(message);
const isStarsGift = message.content.action?.type === 'giftStars';
const isStarGift = message.content.action?.type === 'starGift';
const isPrizeStars = message.content.action?.type === 'prizeStars';
useEffect(() => {
@ -207,7 +219,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
};
const handleStarGiftClick = () => {
openStarsTransactionFromGift({
openGiftInfoModalFromMessage({
chatId: message.chatId,
messageId: message.id,
});
@ -255,6 +267,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
}
function renderGift() {
const giftMessage = message.content.action?.message;
return (
<span
className="action-message-gift"
@ -273,8 +286,16 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
<span>
{oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))}
</span>
{giftMessage && (
<div className="action-message-gift-subtitle">
{renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
</div>
)}
<span className="action-message-button">{oldLang('ActionGiftPremiumView')}</span>
<span className="action-message-button">
<Sparkles preset="button" />
{oldLang('ActionGiftPremiumView')}
</span>
</span>
);
}
@ -282,6 +303,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
function renderGiftCode() {
const isFromGiveaway = message.content.action?.isGiveaway;
const isUnclaimed = message.content.action?.isUnclaimed;
const giftMessage = message.content.action?.message;
return (
<span
className="action-message-gift action-message-gift-code"
@ -316,9 +338,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
), ['simple_markdown'])}
</span>
<span className="action-message-button">{
oldLang('BoostingReceivedGiftOpenBtn')
}
{giftMessage && (
<div className="action-message-gift-subtitle">
{renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
</div>
)}
<span className="action-message-button">
{oldLang('BoostingReceivedGiftOpenBtn')}
</span>
</span>
);
@ -334,7 +361,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
>
<AnimatedIconFromSticker
key={message.id}
sticker={starGiftSticker}
sticker={starsGiftSticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
@ -350,14 +377,124 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
['simple_markdown'],
)}
</span>
<span className="action-message-button">{
oldLang('ActionGiftPremiumView')
}
<span className="action-message-button">
<Sparkles preset="button" />
{oldLang('ActionGiftPremiumView')}
</span>
</span>
);
}
function renderStarGiftUserCaption() {
const targetUser = targetUsers && targetUsers[0];
if (!targetUser || !senderUser) return undefined;
if (message.isOutgoing) {
return (
<div className="action-message-user-caption">
<span> {lang('GiftTo')} </span>
<Avatar className="action-message-user-avatar" size="micro" peer={targetChat} />
<span> {targetUser.firstName} </span>
</div>
);
}
return (
<div className="action-message-user-caption">
<span> {lang('GiftFrom')} </span>
<Avatar className="action-message-user-avatar" size="micro" peer={senderUser} />
<span> {senderUser.firstName} </span>
</div>
);
}
function renderStarGiftUserDescription() {
const starGift = message.content.action?.starGift;
const targetUser = targetUsers && targetUsers[0]?.firstName;
const starGiftMessage = message.content.action?.starGift?.message;
if (!starGift) return undefined;
if (starGiftMessage) {
return renderTextWithEntities({ text: starGiftMessage.text, entities: starGiftMessage.entities });
}
const amount = starGift?.starsToConvert;
if (message.isOutgoing) {
return lang('ActionStarGiftOutDescription', {
user: targetUser || 'User',
count: amount,
}, { withNodes: true });
}
if (starGift.isSaved) {
return lang('ActionStarGiftDisplaying');
}
if (starGift.isConverted) {
return message.isOutgoing
? lang('GiftInfoDescriptionOutConverted', {
amount: formatInteger(amount!),
user: targetUser || 'User',
}, {
pluralValue: amount,
withNodes: true,
withMarkdown: true,
})
: lang('GiftInfoDescriptionConverted', {
amount: formatInteger(amount!),
}, {
pluralValue: amount,
withNodes: true,
withMarkdown: true,
});
}
return lang('ActionStarGiftDescription', {
count: amount,
}, { withNodes: true });
}
function renderStarGift() {
const starGift = message.content.action?.starGift;
if (!starGift) return undefined;
return (
<span
className="action-message-gift action-message-gift-code action-message-star-gift"
tabIndex={0}
role="button"
onClick={handleStarGiftClick}
>
<AnimatedIconFromSticker
sticker={starGiftSticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
size={STAR_GIFT_STICKER_SIZE}
/>
{renderStarGiftUserCaption()}
<div className="action-message-gift-subtitle">
{renderStarGiftUserDescription()}
</div>
{!message.isOutgoing && (
<div className="action-message-button">
<Sparkles preset="button" />
{oldLang('ActionGiftPremiumView')}
</div>
)}
{starGift.gift.availabilityTotal && (
<GiftRibbon
color={patternColor || 'blue'}
text={oldLang('Gift2Limited1OfRibbon', formatIntegerCompact(starGift.gift.availabilityTotal))}
/>
)}
</span>
);
}
function renderPrizeStars() {
const isUnclaimed = message.content.action?.isUnclaimed;
@ -427,6 +564,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
{isPremiumGift && renderGift()}
{isGiftCode && renderGiftCode()}
{isStarsGift && renderStarsGift()}
{isStarGift && renderStarGift()}
{isPrizeStars && renderPrizeStars()}
{isSuggestedAvatar && (
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
@ -458,6 +596,11 @@ export default memo(withGlobal<OwnProps>(
? selectChatMessage(global, chatId, targetMessageId)
: undefined;
const theme = selectTheme(global);
const {
patternColor,
} = global.settings.themes[theme] || {};
const isFocused = threadId ? selectIsMessageFocused(global, message, threadId) : false;
const {
direction: focusDirection,
@ -472,8 +615,10 @@ export default memo(withGlobal<OwnProps>(
const giftDuration = content.action?.months;
const premiumGiftSticker = selectGiftStickerForDuration(global, giftDuration);
const starGift = content.action?.type === 'starGift' ? content.action.starGift?.gift : undefined;
const starCount = content.action?.stars;
const starGiftSticker = selectGiftStickerForStars(global, starCount);
const starGiftSticker = starGift?.stickerId ? selectStarGiftSticker(global, starGift.stickerId) : undefined;
const starsGiftSticker = selectGiftStickerForStars(global, starCount);
const topic = selectTopicFromMessage(global, message);
@ -487,7 +632,9 @@ export default memo(withGlobal<OwnProps>(
isFocused,
premiumGiftSticker,
starGiftSticker,
starsGiftSticker,
topic,
patternColor,
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
...(isFocused && {
focusDirection,

View File

@ -111,7 +111,7 @@ type StateProps = {
canAddContact?: boolean;
canReportChat?: boolean;
canDeleteChat?: boolean;
canGiftPremium?: boolean;
canGift?: boolean;
canCreateTopic?: boolean;
canEditTopic?: boolean;
hasLinkedChat?: boolean;
@ -158,7 +158,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
isMuted,
canReportChat,
canDeleteChat,
canGiftPremium,
canGift,
hasLinkedChat,
canAddContact,
canCreateTopic,
@ -191,7 +191,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
toggleStatistics,
openMonetizationStatistics,
openBoostStatistics,
openPremiumGiftModal,
openGiftModal,
openThreadWithInfo,
openCreateTopicPanel,
openEditTopicPanel,
@ -317,8 +317,8 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
closeMenu();
});
const handleGiftPremiumClick = useLastCallback(() => {
openPremiumGiftModal({ forUserIds: [chatId] });
const handleGiftClick = useLastCallback(() => {
openGiftModal({ forUserId: chatId });
closeMenu();
});
@ -672,12 +672,12 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
</MenuItem>
)}
{botButtons}
{canGiftPremium && (
{canGift && (
<MenuItem
icon="gift"
onClick={handleGiftPremiumClick}
onClick={handleGiftClick}
>
{lang('GiftPremium')}
{lang('ProfileSendAGift')}
</MenuItem>
)}
{isBot && (
@ -756,10 +756,7 @@ export default memo(withGlobal<OwnProps>(
const userFullInfo = isPrivate ? selectUserFullInfo(global, chatId) : undefined;
const chatFullInfo = !isPrivate ? selectChatFullInfo(global, chatId) : undefined;
const fullInfo = userFullInfo || chatFullInfo;
const canGiftPremium = Boolean(
userFullInfo?.premiumGifts?.length
&& !selectIsPremiumPurchaseBlocked(global),
);
const canGift = !selectIsPremiumPurchaseBlocked(global) && !isChatWithSelf;
const topic = selectTopic(global, chatId, threadId);
const canCreateTopic = chat.isForum && (
@ -783,7 +780,7 @@ export default memo(withGlobal<OwnProps>(
canAddContact,
canReportChat,
canDeleteChat: getCanDeleteChat(chat),
canGiftPremium,
canGift,
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
botCommands: chatBot ? userFullInfo?.botInfo?.commands : undefined,
botPrivacyPolicyUrl: chatBot ? userFullInfo?.botInfo?.privacyPolicyUrl : undefined,

View File

@ -271,6 +271,7 @@
.action-message-gift {
display: flex !important;
width: 13.75rem;
flex-direction: column;
align-items: center;
line-height: 1rem !important;
@ -281,15 +282,10 @@
}
.action-message-gift-code {
width: 12rem;
margin-inline: auto;
}
.action-message-stars-gift {
width: 15rem;
margin-inline: auto;
}
.action-message-user-caption,
.action-message-stars-balance {
margin-top: 0.5rem;
display: flex;
@ -298,15 +294,26 @@
font-weight: 500;
}
.action-message-user-caption {
align-items: center;
font-size: 0.875rem;
font-weight: 500;
}
.action-message-user-avatar {
margin-left: 0.25rem;
}
.action-message-subtitle {
margin-top: 1rem;
font-weight: normal;
text-wrap: balance;
}
.action-message-stars-subtitle {
.action-message-gift-subtitle {
font-weight: normal;
text-wrap: balance;
font-size: 0.75rem;
}
.action-message-suggested-avatar {
@ -328,6 +335,7 @@
}
.action-message-button {
position: relative;
display: inline-block;
border-radius: var(--border-radius-default);
padding: 0.5rem 0.75rem;

View File

@ -86,8 +86,6 @@ import Composer from '../common/Composer';
import PrivacySettingsNoticeModal from '../common/PrivacySettingsNoticeModal.async';
import SeenByModal from '../common/SeenByModal.async';
import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
import PremiumGiftModal from '../main/premium/PremiumGiftModal.async';
import StarsGiftModal from '../main/premium/StarsGiftModal.async';
import Button from '../ui/Button';
import Transition from '../ui/Transition';
import ChatLanguageModal from './ChatLanguageModal.async';
@ -138,8 +136,6 @@ type StateProps = {
isSeenByModalOpen: boolean;
isPrivacySettingsNoticeModalOpen: boolean;
isReactorListModalOpen: boolean;
isPremiumGiftModalOpen?: boolean;
isStarsGiftModalOpen?: boolean;
isChatLanguageModalOpen?: boolean;
withInterfaceAnimations?: boolean;
shouldSkipHistoryAnimations?: boolean;
@ -199,8 +195,6 @@ function MiddleColumn({
isSeenByModalOpen,
isPrivacySettingsNoticeModalOpen,
isReactorListModalOpen,
isPremiumGiftModalOpen,
isStarsGiftModalOpen,
isChatLanguageModalOpen,
withInterfaceAnimations,
shouldSkipHistoryAnimations,
@ -721,8 +715,6 @@ function MiddleColumn({
/>
))}
</div>
<PremiumGiftModal isOpen={isPremiumGiftModalOpen} />
<StarsGiftModal isOpen={isStarsGiftModalOpen} />
</div>
);
}
@ -736,7 +728,7 @@ export default memo(withGlobal<OwnProps>(
const {
messageLists, isLeftColumnShown, activeEmojiInteractions,
seenByModal, giftModal, starsGiftModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
seenByModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
chatLanguageModal, privacySettingsNoticeModal,
} = selectTabState(global);
const currentMessageList = selectCurrentMessageList(global);
@ -755,8 +747,6 @@ export default memo(withGlobal<OwnProps>(
isSeenByModalOpen: Boolean(seenByModal),
isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal),
isReactorListModalOpen: Boolean(reactorModal),
isPremiumGiftModalOpen: giftModal?.isOpen,
isStarsGiftModalOpen: starsGiftModal?.isOpen,
isChatLanguageModalOpen: Boolean(chatLanguageModal),
withInterfaceAnimations: selectCanAnimateInterface(global),
currentTransitionKey: Math.max(0, messageLists.length - 1),

View File

@ -46,7 +46,7 @@ const Invoice: FC<OwnProps> = ({
const {
title,
text,
description,
amount,
currency,
isTest,
@ -93,8 +93,8 @@ const Invoice: FC<OwnProps> = ({
{title && (
<p className="title">{renderText(title)}</p>
)}
{text && (
<div>{renderText(text, ['emoji', 'br'])}</div>
{description && (
<div>{renderText(description, ['emoji', 'br'])}</div>
)}
<div className={`description ${photo ? 'has-image' : ''}`}>
{Boolean(photo) && (

View File

@ -19,8 +19,8 @@
}
&.paid.chosen {
--reaction-background: #FFBC2E !important;
--reaction-background-hover: #FFBC2ECC !important;
--reaction-background: #FFB727 !important;
--reaction-background-hover: #FFB727CC !important;
--reaction-text-color: #FFFFFF !important;
}

View File

@ -184,6 +184,7 @@ const ReactionButton = ({
>
{reaction.reaction.type === 'paid' ? (
<>
<Sparkles preset="button" />
<PaidReactionEmoji
className={styles.animatedEmoji}
containerId={containerId}
@ -192,7 +193,6 @@ const ReactionButton = ({
localAmount={reaction.localAmount}
observeIntersection={observeIntersection}
/>
<Sparkles preset="reaction" />
{shouldRenderPaidCounter && (
<AnimatedCounter
ref={counterRef}

View File

@ -11,12 +11,16 @@ import BoostModal from './boost/BoostModal.async';
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
import ChatlistModal from './chatlist/ChatlistModal.async';
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
import PremiumGiftModal from './gift/GiftModal.async';
import GiftInfoModal from './gift/info/GiftInfoModal.async';
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
import GiftCodeModal from './giftcode/GiftCodeModal.async';
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
import MapModal from './map/MapModal.async';
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async';
import StarsGiftModal from './stars/gift/StarsGiftModal.async';
import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async';
import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async';
@ -37,13 +41,17 @@ type ModalKey = keyof Pick<TabState,
'collectibleInfoModal' |
'reportAdModal' |
'starsBalanceModal' |
'isStarPaymentModalOpen' |
'starsPayment' |
'starsTransactionModal' |
'paidReactionModal' |
'webApps' |
'starsTransactionModal' |
'chatInviteModal' |
'starsSubscriptionModal'
'starsSubscriptionModal' |
'starsGiftModal' |
'giftModal' |
'isGiftRecipientPickerOpen' |
'giftInfoModal'
>;
type StateProps = {
@ -70,12 +78,16 @@ const MODALS: ModalRegistry = {
webApps: WebAppModal,
collectibleInfoModal: CollectibleInfoModal,
mapModal: MapModal,
isStarPaymentModalOpen: StarsPaymentModal,
starsPayment: StarsPaymentModal,
starsBalanceModal: StarsBalanceModal,
starsTransactionModal: StarsTransactionInfoModal,
chatInviteModal: ChatInviteModal,
paidReactionModal: PaidReactionModal,
starsSubscriptionModal: StarsSubscriptionModal,
starsGiftModal: StarsGiftModal,
giftModal: PremiumGiftModal,
isGiftRecipientPickerOpen: GiftRecipientPicker,
giftInfoModal: GiftInfoModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -29,6 +29,10 @@
overflow: hidden;
}
.noFooter {
margin-top: 1.5rem;
}
.cell {
display: flex;
align-items: center;
@ -37,6 +41,10 @@
min-height: 2.5rem;
}
.fullWidth {
grid-column: 1 / -1;
}
.avatar {
align-self: center;
}

View File

@ -17,7 +17,7 @@ import styles from './TableInfoModal.module.scss';
type ChatItem = { chatId: string };
export type TableData = [TeactNode, TeactNode | ChatItem][];
export type TableData = [TeactNode | undefined, TeactNode | ChatItem][];
type OwnProps = {
isOpen?: boolean;
@ -68,8 +68,8 @@ const TableInfoModal = ({
<div className={styles.table}>
{tableData?.map(([label, value]) => (
<>
<div className={buildClassName(styles.cell, styles.title)}>{label}</div>
<div className={buildClassName(styles.cell, styles.value)}>
{label && <div className={buildClassName(styles.cell, styles.title)}>{label}</div>}
<div className={buildClassName(styles.cell, styles.value, !label && styles.fullWidth)}>
{typeof value === 'object' && 'chatId' in value ? (
<PickerSelectedItem
peerId={value.chatId}
@ -86,7 +86,12 @@ const TableInfoModal = ({
</div>
{footer}
{buttonText && (
<Button size="smaller" onClick={onButtonClick || onClose}>{buttonText}</Button>
<Button
className={!footer ? styles.noFooter : undefined}
size="smaller"
onClick={onButtonClick || onClose}
>{buttonText}
</Button>
)}
</Modal>
);

View File

@ -0,0 +1,142 @@
.root {
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
padding-top: 3.5rem;
}
.header {
padding: 0.5rem;
padding-left: 4rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.spacer {
flex-grow: 1;
}
.title {
margin-inline: 0.5rem;
font-size: 1.25rem;
font-weight: 500;
}
.balance-container {
margin-left: auto;
align-items: end;
display: flex;
flex-direction: column;
}
.balance-caption {
font-size: 1rem;
}
.star-balance {
margin-right: 0.1875rem;
}
.balance {
display: flex;
font-size: 1rem;
font-weight: 500;
align-items: center;
}
.optionsSection {
padding: 1rem;
padding-bottom: 0.5rem;
box-shadow: 0 1px 2px var(--color-default-shadow);
}
.checkboxTitle {
color: var(--color-text);
font-size: 1rem;
text-transform: initial;
margin: 0;
}
.actionMessageView {
display: grid;
place-content: center;
height: 22.5rem;
margin-bottom: 0;
position: relative;
overflow: hidden;
flex: 0 0 auto;
background-color: var(--theme-background-color);
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
:global(html.theme-light) & {
background-image: url('../../../assets/chat-bg-br.png');
}
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-image: url('../../../assets/chat-bg-pattern-light.png');
background-position: top right;
background-size: 510px auto;
background-repeat: repeat;
mix-blend-mode: overlay;
:global(html.theme-dark) & {
background-image: url('../../../assets/chat-bg-pattern-dark.png');
mix-blend-mode: unset;
}
@media (max-width: 600px) {
bottom: auto;
height: calc(var(--vh, 1vh) * 100);
}
}
}
.messageInput, .limited {
margin-bottom: 0.5rem;
}
.footer {
display: flex;
justify-content: space-between;
padding: 1rem;
padding-top: 0.5rem;
flex-grow: 1;
flex-direction: column;
background-color: var(--color-background-secondary);
}
.switcher {
margin-bottom: 0 !important;
}
.description {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.main-button {
display: flex;
font-weight: 500;
font-size: 1rem;
height: 3rem;
}
.star {
--color-fill: var(--color-white);
width: 1rem;
height: 1rem;
margin-right: 0.1875rem;
margin-left: 0.5rem;
}

View File

@ -0,0 +1,259 @@
import type { ChangeEvent } from 'react';
import React, {
memo, useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiMessage, ApiUser } from '../../../api/types';
import type { GiftOption } from './GiftModal';
import { STARS_CURRENCY_CODE, STARS_ICON_PLACEHOLDER } from '../../../config';
import { getUserFullName } from '../../../global/helpers';
import { selectTabState, selectTheme, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatCurrency } from '../../../util/formatCurrency';
import { formatInteger } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/icons/Icon';
import PremiumProgress from '../../common/PremiumProgress';
import ActionMessage from '../../middle/ActionMessage';
import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem';
import Switcher from '../../ui/Switcher';
import TextArea from '../../ui/TextArea';
import styles from './GiftComposer.module.scss';
export type OwnProps = {
gift: GiftOption;
userId: string;
};
export type StateProps = {
captionLimit?: number;
patternColor?: string;
user?: ApiUser;
currentUserId?: string;
isPaymentFormLoading?: boolean;
};
const LIMIT_DISPLAY_THRESHOLD = 50;
function GiftComposer({
gift,
userId,
user,
captionLimit,
patternColor,
currentUserId,
isPaymentFormLoading,
}: OwnProps & StateProps) {
const { sendStarGift, openInvoice } = getActions();
const lang = useLang();
const [giftMessage, setGiftMessage] = useState<string>('');
const [shouldHideName, setShouldHideName] = useState<boolean>(false);
const isStarGift = 'id' in gift;
const localMessage = useMemo(() => {
if (!isStarGift) {
return {
id: -1,
chatId: '0',
isOutgoing: true,
senderId: currentUserId,
date: Math.floor(Date.now() / 1000),
content: {
action: {
targetUserIds: [userId],
mediaType: 'action',
text: 'ActionGiftInbound',
type: 'giftPremium',
amount: gift.amount,
currency: gift.currency,
months: gift.months,
message: {
text: giftMessage,
},
translationValues: ['%action_origin%', '%gift_payment_amount%'],
},
},
} satisfies ApiMessage;
}
return {
id: -1,
chatId: currentUserId!,
isOutgoing: false,
senderId: currentUserId,
date: Math.floor(Date.now() / 1000),
content: {
action: {
targetUserIds: [userId],
mediaType: 'action',
text: 'ActionGiftInbound',
type: 'starGift',
currency: STARS_CURRENCY_CODE,
amount: gift.stars,
starGift: {
message: giftMessage?.length ? {
text: giftMessage,
} : undefined,
isNameHidden: shouldHideName,
starsToConvert: gift.starsToConvert,
isSaved: false,
isConverted: false,
gift,
},
translationValues: ['%action_origin%', '%gift_payment_amount%'],
},
},
} satisfies ApiMessage;
}, [currentUserId, gift, giftMessage, isStarGift, shouldHideName, userId]);
const handleGiftMessageChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setGiftMessage(e.target.value);
});
const handleShouldHideNameChange = useLastCallback(() => {
setShouldHideName(!shouldHideName);
});
const handleMainButtonClick = useLastCallback(() => {
if (isStarGift) {
sendStarGift({
userId,
shouldHideName,
gift,
message: giftMessage ? { text: giftMessage } : undefined,
});
return;
}
openInvoice({
type: 'giftcode',
userIds: [userId],
currency: gift.currency,
amount: gift.amount,
option: gift,
message: giftMessage ? { text: giftMessage } : undefined,
});
});
function renderOptionsSection() {
const symbolsLeft = captionLimit ? captionLimit - giftMessage.length : undefined;
return (
<div className={styles.optionsSection}>
<TextArea
className={styles.messageInput}
onChange={handleGiftMessageChange}
value={giftMessage}
label={lang('GiftMessagePlaceholder')}
maxLength={captionLimit}
maxLengthIndicator={symbolsLeft && symbolsLeft < LIMIT_DISPLAY_THRESHOLD ? symbolsLeft.toString() : undefined}
/>
{isStarGift && (
<ListItem className={styles.switcher} narrow ripple onClick={handleShouldHideNameChange}>
<span>{lang('GiftHideMyName')}</span>
<Switcher
checked={shouldHideName}
onChange={handleShouldHideNameChange}
label={lang('GiftHideMyName')}
/>
</ListItem>
)}
</div>
);
}
function renderFooter() {
const userFullName = getUserFullName(user)!;
const amount = isStarGift ? (
lang('StarsAmount', {
amount: formatInteger(gift.stars),
}, {
withNodes: true,
specialReplacement: {
[STARS_ICON_PLACEHOLDER]: <Icon className="star-amount-icon" name="star" />,
},
})
) : formatCurrency(gift.amount, gift.currency);
return (
<div className={styles.footer}>
{isStarGift && (
<div className={styles.description}>
{lang('GiftHideNameDescription', { profile: userFullName, receiver: userFullName })}
</div>
)}
<div className={styles.spacer} />
{isStarGift && gift.availabilityRemains && (
<PremiumProgress
isPrimary
progress={gift.availabilityRemains / gift.availabilityTotal!}
rightText={lang('GiftSoldCount', {
count: formatInteger(gift.availabilityTotal! - gift.availabilityRemains),
})}
leftText={lang('GiftLeftCount', { count: formatInteger(gift.availabilityRemains) })}
className={styles.limited}
/>
)}
<Button
className={styles.mainButton}
onClick={handleMainButtonClick}
isLoading={isPaymentFormLoading}
>
{lang('GiftSend', {
amount,
}, {
withNodes: true,
})}
</Button>
</div>
);
}
return (
<div className={buildClassName(styles.root, 'no-scroll')}>
<div
className={buildClassName(styles.actionMessageView, 'MessageList')}
// @ts-ignore -- FIXME: Find a way to disable interactions but keep a11y
inert
style={`--pattern-color: ${patternColor}`}
>
<ActionMessage key={isStarGift ? gift.id : gift.months} message={localMessage} />
</div>
{renderOptionsSection()}
{renderFooter()}
</div>
);
}
export default memo(withGlobal<OwnProps>(
(global, { userId }): StateProps => {
const theme = selectTheme(global);
const {
patternColor,
} = global.settings.themes[theme] || {};
const user = selectUser(global, userId);
const tabState = selectTabState(global);
return {
user,
patternColor,
captionLimit: global.appConfig?.starGiftMaxMessageLength,
currentUserId: global.currentUserId,
isPaymentFormLoading: tabState.isPaymentFormLoading,
};
},
)(GiftComposer));

View File

@ -0,0 +1,65 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
flex-basis: 0;
min-width: 0;
padding: 0.625rem;
padding-top: 0;
border-radius: 0.625rem;
background-color: var(--color-background-secondary);
position: relative;
cursor: var(--custom-cursor, pointer);
&::before {
content: "";
position: absolute;
inset: 0;
opacity: 0;
border-radius: 0.625rem;
background-color: var(--color-hover-overlay);
pointer-events: none;
}
&:hover::before {
opacity: 1;
}
}
.starGift {
padding: 0.875rem;
padding-bottom: 0.625rem;
}
.monthsDescription {
margin-top: 0.25rem;
font-weight: 500;
}
.description {
font-size: 0.875rem;
line-height: 1;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.buy {
font-size: 0.6875rem !important;
margin-top: 0.625rem;
line-height: 1;
font-weight: 400 !important;
}
.star {
margin-inline-end: 0.125rem;
font-size: 0.75rem !important;
}
.amount {
margin-top: 0.0625rem; // It just refuses to be centered
}

View File

@ -0,0 +1,102 @@
import React, { memo } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type {
ApiPremiumGiftCodeOption,
ApiSticker,
} from '../../../api/types';
import {
selectCanPlayAnimatedEmojis,
selectGiftStickerForDuration,
} from '../../../global/selectors';
import { formatCurrencyAsString } from '../../../util/formatCurrency';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
import GiftRibbon from '../../common/gift/GiftRibbon';
import Button from '../../ui/Button';
import styles from './GiftItem.module.scss';
export type OwnProps = {
option: ApiPremiumGiftCodeOption;
baseMonthAmount?: number;
onClick: (gift: ApiPremiumGiftCodeOption) => void;
};
export type StateProps = {
sticker?: ApiSticker;
canPlayAnimatedEmojis?: boolean;
};
const GIFT_STICKER_SIZE = 86;
function GiftItemPremium({
sticker, canPlayAnimatedEmojis, baseMonthAmount, option, onClick,
}: OwnProps & StateProps) {
const {
months, amount, currency,
} = option;
const lang = useLang();
const handleGiftClick = useLastCallback(() => {
onClick(option);
});
const perMonth = Math.floor(amount / months);
const discount = baseMonthAmount && baseMonthAmount > perMonth
? Math.ceil(100 - perMonth / (baseMonthAmount / 100))
: undefined;
function renderMonths() {
const caption = months === 12 ? lang('Years', { count: 1 }) : lang('Months', { count: months });
return (
<div className={styles.monthsDescription}>
{caption}
</div>
);
}
return (
<div
className={styles.container}
tabIndex={0}
role="button"
onClick={handleGiftClick}
>
{Boolean(discount) && (
<GiftRibbon color="red" text={lang('GiftDiscount', { percent: discount })} />
)}
<AnimatedIconFromSticker
sticker={sticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
size={GIFT_STICKER_SIZE}
/>
{renderMonths()}
<div className={styles.description}>
{lang('PremiumGiftDescription')}
</div>
<Button className={styles.buy} nonInteractive size="tiny" pill fluid>
{formatCurrencyAsString(amount, currency)}
</Button>
</div>
);
}
export default memo(withGlobal<OwnProps>(
(global, { option }): StateProps => {
const sticker = selectGiftStickerForDuration(global, option.months);
const canPlayAnimatedEmojis = selectCanPlayAnimatedEmojis(global);
return {
sticker,
canPlayAnimatedEmojis,
};
},
)(GiftItemPremium));

View File

@ -0,0 +1,89 @@
import React, { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiStarGift,
ApiSticker,
} from '../../../api/types';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
import GiftRibbon from '../../common/gift/GiftRibbon';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import styles from './GiftItem.module.scss';
export type OwnProps = {
gift: ApiStarGift;
onClick: (gift: ApiStarGift) => void;
};
export type StateProps = {
sticker?: ApiSticker;
};
const GIFT_STICKER_SIZE = 90;
function GiftItemStar({ sticker, gift, onClick }: OwnProps & StateProps) {
const { showNotification } = getActions();
const lang = useLang();
const {
stars,
isLimited,
availabilityRemains,
availabilityTotal,
} = gift;
const isSoldOut = availabilityTotal && !availabilityRemains;
const handleGiftClick = useLastCallback(() => {
if (isSoldOut) {
showNotification({ message: lang('GiftSoldOutInfo') });
return;
}
onClick(gift);
});
if (!sticker) return undefined;
return (
<div
className={buildClassName(styles.container, styles.starGift)}
tabIndex={0}
role="button"
onClick={handleGiftClick}
>
{isLimited && !isSoldOut && <GiftRibbon color="blue" text={lang('GiftLimited')} />}
{isSoldOut && <GiftRibbon color="red" text={lang('GiftSoldOut')} />}
<AnimatedIconFromSticker
sticker={sticker}
noLoop
nonInteractive
size={GIFT_STICKER_SIZE}
/>
<Button className={styles.buy} nonInteractive size="tiny" color="sparkles" withSparkleEffect pill fluid>
<Icon name="star" className={styles.star} />
<div className={styles.amount}>
{stars}
</div>
</Button>
</div>
);
}
export default memo(withGlobal<OwnProps>(
(global, { gift }): StateProps => {
const sticker = global.stickers.starGifts.stickers[gift.stickerId];
return {
sticker,
};
},
)(GiftItemStar));

View File

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

View File

@ -1,21 +1,19 @@
@use '../../../styles/mixins';
@media (min-width: 451px) {
.modalDialog :global(.modal-dialog) {
max-width: 32rem !important;
}
}
.root {
z-index: calc(var(--z-modal-low-priority) + 1);
}
.root :global(.modal-content) {
padding: 0;
background-color: var(--color-background);
}
.root :global(.modal-dialog) {
max-width: 26.25rem;
.root :global(.modal-dialog),
.transition,
.content {
height: min(92vh, 45rem);
max-height: none !important;
}
.root :global(.modal-dialog),
@ -31,6 +29,11 @@
.main {
overflow-y: scroll;
height: 100%;
padding-bottom: 1rem;
padding-inline: 1rem;
@include mixins.adapt-padding-to-scrollbar(1rem);
}
.giftSection {
@ -41,6 +44,23 @@
padding: 0.5rem;
}
.starGiftsContainer,
.premiumGiftsGallery {
display: flex;
justify-content: center;
align-items: center;
gap: 0.625rem;
margin-bottom: 0.75rem;
}
.starGiftsContainer {
display: grid;
grid-template-columns: repeat(3, 1fr);
margin: 0rem;
padding: 0.125rem;
padding-top: 0.75rem;
}
.header {
z-index: 2;
display: flex;
@ -54,49 +74,77 @@
padding: 0.5rem;
background: var(--color-background);
transition: 0.25s ease-out transform;
overflow: hidden;
}
.starHeaderText {
font-size: 1.25rem;
.headerSlide {
display: flex;
align-items: center;
}
.headerText {
font-size: 1.5rem;
font-weight: 500;
margin: 0 0 0 3rem;
unicode-bidi: plaintext;
margin-bottom: 0;
}
.hiddenHeader {
transform: translateY(-100%);
}
.commonHeaderText {
font-size: 1.25rem;
font-weight: 500;
margin: 0 0 0 3rem;
unicode-bidi: plaintext;
}
.closeButton {
position: absolute;
top: 0.5rem;
left: 0.5rem;
top: 0.375rem;
left: 0.375rem;
z-index: 3;
}
.balance {
position: absolute;
top: 0.75rem;
right: 1.25rem;
z-index: 3;
}
.avatars {
display: flex;
position: relative;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
padding: 1rem;
margin-top: 1rem;
z-index: 1;
}
.logoBackground {
position: absolute;
left: 50%;
transform: translateX(-50%);
height: 7rem;
}
.center {
text-align: center;
}
.description,
.premiumFeatures {
.description {
text-align: center;
margin: 0 auto 2rem;
line-height: 1.375;
margin: 0.25rem 1rem 1rem;
max-width: 25rem;
}
.premiumFeatures {
font-size: 0.9375rem;
color: var(--color-text-secondary);
.starGiftsDescription {
margin-bottom: 0.625rem;
}
.boostIcon {
@ -120,3 +168,9 @@
.footer {
margin: 0 1.5rem 1rem;
}
.starGiftsTransition {
overflow: hidden;
min-height: calc(100% - 3.5rem);
height: auto;
}

View File

@ -0,0 +1,334 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiPremiumGiftCodeOption,
ApiStarGift,
ApiUser,
} from '../../../api/types';
import type { StarGiftCategory, TabState } from '../../../global/types';
import { getUserFullName } from '../../../global/helpers';
import { selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import SafeLink from '../../common/SafeLink';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import Transition from '../../ui/Transition';
import BalanceBlock from '../stars/BalanceBlock';
import GiftSendingOptions from './GiftComposer';
import GiftItemPremium from './GiftItemPremium';
import GiftItemStar from './GiftItemStar';
import StarGiftCategoryList from './StarGiftCategoryList';
import styles from './GiftModal.module.scss';
import StarsBackground from '../../../assets/stars-bg.png';
export type OwnProps = {
modal: TabState['giftModal'];
};
export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGift;
type StateProps = {
boostPerSentGift?: number;
starGiftsById?: Record<string, ApiStarGift>;
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
starBalance?: number;
user?: ApiUser;
};
const PremiumGiftModal: FC<OwnProps & StateProps> = ({
modal,
starGiftsById,
starGiftCategoriesByName,
starBalance,
user,
}) => {
const {
closeGiftModal, requestConfetti,
} = getActions();
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const transitionRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const giftHeaderRef = useRef<HTMLHeadingElement>(null);
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const [selectedGift, setSelectedGift] = useState<GiftOption | undefined>();
const [isHeaderHidden, setIsHeaderHidden] = useState(true);
const [isHeaderForStarGifts, setIsHeaderForStarGifts] = useState(false);
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
const oldLang = useOldLang();
const lang = useLang();
const filteredGifts = useMemo(() => {
return renderingModal?.gifts?.sort((prevGift, gift) => prevGift.months - gift.months)
.filter((gift) => gift.users === 1);
}, [renderingModal]);
const baseGift = useMemo(() => {
return filteredGifts?.reduce((prev, gift) => (prev.amount < gift.amount ? prev : gift));
}, [filteredGifts]);
const showConfetti = useLastCallback(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
const {
top, left, width, height,
} = dialog.querySelector('.modal-content')!.getBoundingClientRect();
requestConfetti({
top,
left,
width,
height,
withStars: true,
});
}
});
useEffect(() => {
if (renderingModal?.isCompleted) {
showConfetti();
}
}, [renderingModal]);
useEffect(() => {
if (!isOpen) {
setIsHeaderHidden(true);
setSelectedGift(undefined);
}
}, [isOpen]);
const handleScroll = useLastCallback((e: React.UIEvent<HTMLDivElement>) => {
if (selectedGift) return;
const { scrollTop } = e.currentTarget;
setIsHeaderHidden(scrollTop <= 150);
if (transitionRef.current && giftHeaderRef.current) {
const { top: headerTop } = giftHeaderRef.current.getBoundingClientRect();
const { top: transitionTop } = transitionRef.current.getBoundingClientRect();
setIsHeaderForStarGifts(headerTop - transitionTop <= 0);
}
});
const giftPremiumDescription = lang('GiftPremiumDescription', {
user: getUserFullName(user)!,
link: (
<SafeLink
text={lang('GiftPremiumDescriptionLinkCaption')}
url={lang('GiftPremiumDescriptionLink')}
/>
),
}, { withNodes: true });
const starGiftDescription = lang('StarGiftDescription', {
user: getUserFullName(user)!,
}, { withNodes: true });
function renderGiftPremiumHeader() {
return (
<h2 className={buildClassName(styles.headerText, styles.center)}>
{lang('GiftPremiumHeader')}
</h2>
);
}
function renderGiftPremiumDescription() {
return (
<p className={buildClassName(styles.description, styles.center)}>
{giftPremiumDescription}
</p>
);
}
function renderStarGiftsHeader() {
return (
<h2 ref={giftHeaderRef} className={buildClassName(styles.headerText, styles.center)}>
{lang('StarsGiftHeader')}
</h2>
);
}
function renderStarGiftsDescription() {
return (
<p className={buildClassName(styles.description, styles.starGiftsDescription, styles.center)}>
{starGiftDescription}
</p>
);
}
const handleGiftClick = useLastCallback((gift: GiftOption) => {
setSelectedGift(gift);
setIsHeaderForStarGifts('id' in gift);
setIsHeaderHidden(false);
});
function renderStarGifts() {
return (
<div className={styles.starGiftsContainer}>
{starGiftsById && starGiftCategoriesByName[selectedCategory].map((giftId) => {
const gift = starGiftsById[giftId];
return (
<GiftItemStar
gift={gift}
onClick={handleGiftClick}
/>
);
})}
</div>
);
}
function renderPremiumGifts() {
return (
<div className={styles.premiumGiftsGallery}>
{filteredGifts?.map((gift) => {
return (
<GiftItemPremium
option={gift}
baseMonthAmount={baseGift ? Math.floor(baseGift.amount / baseGift.months) : undefined}
onClick={handleGiftClick}
/>
);
})}
</div>
);
}
const onCategoryChanged = useLastCallback((category: StarGiftCategory) => {
setSelectedCategory(category);
});
const handleCloseButtonClick = useLastCallback(() => {
if (selectedGift) {
setSelectedGift(undefined);
return;
}
closeGiftModal();
});
function renderMainScreen() {
return (
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
<div className={styles.avatars}>
<Avatar
size="huge"
peer={user}
/>
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />
</div>
{renderGiftPremiumHeader()}
{renderGiftPremiumDescription()}
{renderPremiumGifts()}
{renderStarGiftsHeader()}
{renderStarGiftsDescription()}
<StarGiftCategoryList onCategoryChanged={onCategoryChanged} />
<Transition
name="zoomFade"
activeKey={getCategoryKey(selectedCategory)}
className={styles.starGiftsTransition}
>
{renderStarGifts()}
</Transition>
</div>
);
}
const isBackButton = Boolean(selectedGift);
const buttonClassName = buildClassName(
'animated-close-icon',
isBackButton && 'state-back',
);
return (
<Modal
dialogRef={dialogRef}
onClose={closeGiftModal}
isOpen={isOpen}
isSlim
contentClassName={styles.content}
className={buildClassName(styles.modalDialog, styles.root)}
>
<Button
className={styles.closeButton}
round
color="translucent"
size="smaller"
onClick={handleCloseButtonClick}
ariaLabel={isBackButton ? oldLang('Common.Back') : oldLang('Common.Close')}
>
<div className={buttonClassName} />
</Button>
<BalanceBlock className={styles.balance} balance={starBalance} />
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
<Transition
name="slideVerticalFade"
activeKey={Number(isHeaderForStarGifts)}
slideClassName={styles.headerSlide}
>
<h2 className={styles.commonHeaderText}>
{lang(isHeaderForStarGifts ? 'StarsGiftHeader' : 'GiftPremiumHeader')}
</h2>
</Transition>
</div>
<Transition
ref={transitionRef}
className={styles.transition}
name="pushSlide"
activeKey={selectedGift ? 1 : 0}
>
{!selectedGift && renderMainScreen()}
{selectedGift && renderingModal?.forUserId && (
<GiftSendingOptions gift={selectedGift} userId={renderingModal.forUserId} />
)}
</Transition>
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
const { starGiftsById, starGiftCategoriesByName, stars } = global;
const user = modal?.forUserId ? selectUser(global, modal.forUserId) : undefined;
return {
boostPerSentGift: global.appConfig?.boostsPerSentGift,
starGiftsById,
starGiftCategoriesByName,
starBalance: stars?.balance,
user,
};
})(PremiumGiftModal));
function getCategoryKey(category: StarGiftCategory) {
if (category === 'all') {
return -1;
}
if (category === 'limited') {
return 0;
}
return category;
}

View File

@ -0,0 +1,50 @@
@use '../../../styles/mixins';
.list {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.0625rem;
flex-wrap: nowrap;
background-color: var(--color-background);
overflow-x: auto;
overflow-y: hidden;
z-index: 1;
font-size: 0.875rem;
padding-block: 0.25rem;
justify-content: flex-start;
// Prevent first item from being always partially obscured
margin-left: -0.5rem;
padding-left: 0.5rem;
@include mixins.gradient-border-horizontal(0.5rem, 0.5rem);
}
.item-selected,
.item {
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
width: auto;
padding: 0.375rem 0.75rem;
font-weight: 500;
color: var(--color-text-secondary);
border-radius: 1rem;
&:hover {
cursor: pointer;
background-color: var(--color-background-secondary-accent);
}
}
.selected-item {
background-color: var(--color-background-secondary);
color: var(--color-text-secondary);
}
.star {
margin-right: 0.1875rem;
}

View File

@ -0,0 +1,98 @@
import React, {
memo, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type { StarGiftCategory } from '../../../global/types';
import buildClassName from '../../../util/buildClassName';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLang from '../../../hooks/useLang';
import StarIcon from '../../common/icons/StarIcon';
import styles from './StarGiftCategoryList.module.scss';
type OwnProps = {
onCategoryChanged: (category: StarGiftCategory) => void;
};
type StateProps = {
starGiftCategoriesByName: Record<StarGiftCategory, string[]>;
};
const StarGiftCategoryList = ({
starGiftCategoriesByName,
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)
.filter((category) => category !== 'all' && category !== 'limited')
.map(Number)
.sort((a, b) => a - b),
[starGiftCategoriesByName]);
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
function handleItemClick(category: StarGiftCategory) {
setSelectedCategory(category);
onCategoryChanged(
category,
);
}
function renderCategoryName(category: StarGiftCategory) {
if (category === 'all') {
return lang('AllGiftsCategory');
}
if (category === 'limited') {
return lang('LimitedGiftsCategory');
}
return category;
}
function renderCategoryItem(category: StarGiftCategory) {
return (
<div
className={buildClassName(
styles.item,
selectedCategory === category && styles.selectedItem,
)}
onClick={() => handleItemClick(category)}
>
{category !== 'all' && category !== 'limited' && (
<StarIcon
className={styles.star}
type="gold"
size="middle"
/>
)}
{renderCategoryName(category)}
</div>
);
}
useHorizontalScroll(ref, undefined, true);
return (
<div ref={ref} className={buildClassName(styles.list, 'no-scrollbar')}>
{renderCategoryItem('all')}
{renderCategoryItem('limited')}
{starCategories.map(renderCategoryItem)}
</div>
);
};
export default memo(withGlobal(
(global): StateProps => {
const { starGiftCategoriesByName } = global;
return {
starGiftCategoriesByName,
};
},
)(StarGiftCategoryList));

View File

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

View File

@ -0,0 +1,42 @@
.header {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
position: relative;
}
.amount {
display: flex;
gap: 0.25rem;
font-size: 1rem;
font-weight: 500;
line-height: 1.325;
}
.title, .description, .amount {
margin-bottom: 0;
}
.description {
text-align: center;
}
.footerDescription {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-align: center;
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.unknown {
margin-inline-start: 0.25rem;
}
.giftValue {
display: flex;
align-items: center;
gap: 0.125rem;
}

View File

@ -0,0 +1,274 @@
import React, { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiSticker, ApiUser } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { getUserFullName } from '../../../../global/helpers';
import { selectStarGiftSticker, selectUser } from '../../../../global/selectors';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
import { formatInteger } from '../../../../util/textFormat';
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import BadgeButton from '../../../common/BadgeButton';
import StarIcon from '../../../common/icons/StarIcon';
import Button from '../../../ui/Button';
import ConfirmDialog from '../../../ui/ConfirmDialog';
import Link from '../../../ui/Link';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import styles from './GiftInfoModal.module.scss';
export type OwnProps = {
modal: TabState['giftInfoModal'];
};
type StateProps = {
sticker?: ApiSticker;
userFrom?: ApiUser;
targetUser?: ApiUser;
currentUserId?: string;
};
const STICKER_SIZE = 120;
const GiftInfoModal = ({
modal, sticker, userFrom, targetUser, currentUserId,
}: OwnProps & StateProps) => {
const {
closeGiftInfoModal,
changeGiftVisilibity,
convertGiftToStars,
openChatWithInfo,
} = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
const lang = useLang();
const oldLang = useOldLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const { gift: userGift } = renderingModal || {};
const canUpdate = Boolean(userGift?.fromId && userGift.messageId);
const isSender = userGift?.fromId === currentUserId;
const handleClose = useLastCallback(() => {
closeGiftInfoModal();
});
const handleTriggerVisibility = useLastCallback(() => {
const { fromId, messageId, isUnsaved } = userGift!;
changeGiftVisilibity({ userId: fromId!, messageId: messageId!, shouldUnsave: !isUnsaved });
handleClose();
});
const handleConvertToStars = useLastCallback(() => {
const { fromId, messageId } = userGift!;
convertGiftToStars({ userId: fromId!, messageId: messageId! });
closeConvertConfirm();
handleClose();
});
const handleOpenProfile = useLastCallback(() => {
openChatWithInfo({ id: currentUserId!, profileTab: 'gifts' });
handleClose();
});
const modalData = useMemo(() => {
if (!userGift) {
return undefined;
}
const {
gift, date, fromId, isNameHidden, message, starsToConvert, isUnsaved, isConverted,
} = userGift;
const description = (() => {
if (!canUpdate && !isSender) return undefined;
if (isConverted) {
return canUpdate
? lang('GiftInfoDescriptionConverted', {
amount: formatInteger(starsToConvert!),
}, {
pluralValue: starsToConvert,
withNodes: true,
withMarkdown: true,
})
: lang('GiftInfoDescriptionOutConverted', {
amount: formatInteger(starsToConvert!),
user: getUserFullName(targetUser)!,
}, {
pluralValue: starsToConvert,
withNodes: true,
withMarkdown: true,
});
}
return canUpdate
? lang('GiftInfoDescription', {
amount: formatInteger(starsToConvert!),
}, {
withNodes: true,
withMarkdown: true,
})
: lang('GiftInfoDescriptionOut', {
amount: formatInteger(starsToConvert!),
user: getUserFullName(targetUser)!,
}, {
withNodes: true,
withMarkdown: true,
});
})();
const header = (
<div className={styles.header}>
<AnimatedIconFromSticker sticker={sticker} noLoop nonInteractive size={STICKER_SIZE} />
<h1 className={styles.title}>
{lang(canUpdate ? 'GiftInfoReceived' : 'GiftInfoTitle')}
</h1>
<p className={styles.amount}>
<span className={styles.amount}>
{formatInteger(gift.stars)}
</span>
<StarIcon type="gold" size="middle" />
</p>
{description && (
<p className={styles.description}>
{description}
</p>
)}
</div>
);
const tableData: TableData = [];
if (fromId || isNameHidden) {
tableData.push([
lang('GiftInfoFrom'),
fromId ? { chatId: fromId } : (
<>
<Avatar size="small" peer={CUSTOM_PEER_HIDDEN} />
<span className={styles.unknown}>{oldLang(CUSTOM_PEER_HIDDEN.titleKey!)}</span>
</>
),
]);
}
tableData.push([
lang('GiftInfoDate'),
formatDateTimeToString(date * 1000, lang.code, true),
]);
tableData.push([
lang('GiftInfoValue'),
<div className={styles.giftValue}>
{lang('StarsAmount', {
amount: formatInteger(gift.stars),
}, {
withNodes: true,
specialReplacement: {
[STARS_ICON_PLACEHOLDER]: <StarIcon type="gold" size="small" />,
},
})}
{canUpdate && Boolean(starsToConvert) && (
<BadgeButton onClick={openConvertConfirm}>
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
</BadgeButton>
)}
</div>,
]);
if (message) {
tableData.push([
undefined,
renderTextWithEntities(message),
]);
}
const footer = (
<div className={styles.footer}>
{canUpdate && (
<p className={styles.footerDescription}>
{isUnsaved ? lang('GiftInfoHidden')
: lang('GiftInfoSaved', {
link: <Link isPrimary onClick={handleOpenProfile}>{lang('GiftInfoSavedView')}</Link>,
}, {
withNodes: true,
})}
</p>
)}
{!canUpdate && (
<Button size="smaller" onClick={handleClose}>
{lang('OK')}
</Button>
)}
{canUpdate && (
<Button size="smaller" onClick={handleTriggerVisibility}>
{lang(isUnsaved ? 'GiftInfoMakeVisible' : 'GiftInfoMakeInvisible')}
</Button>
)}
</div>
);
return {
header,
tableData,
footer,
};
}, [userGift, sticker, lang, canUpdate, isSender, oldLang, targetUser]);
return (
<>
<TableInfoModal
isOpen={isOpen}
header={modalData?.header}
tableData={modalData?.tableData}
footer={modalData?.footer}
onClose={handleClose}
/>
<ConfirmDialog
isOpen={isConvertConfirmOpen}
onClose={closeConvertConfirm}
confirmHandler={handleConvertToStars}
title={lang('GiftInfoConvertTitle')}
>
{userGift && lang('GiftInfoConvertDescription', {
amount: lang('StarsAmountText', { amount: formatInteger(userGift.starsToConvert!) }),
user: getUserFullName(userFrom)!,
}, {
withNodes: true,
withMarkdown: true,
renderTextFilters: ['br'],
})}
</ConfirmDialog>
</>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const stickerId = modal?.gift?.gift.stickerId;
const sticker = stickerId ? selectStarGiftSticker(global, stickerId) : undefined;
const fromId = modal?.gift?.fromId;
const userFrom = fromId ? selectUser(global, fromId) : undefined;
const targetUser = modal?.userId ? selectUser(global, modal.userId) : undefined;
return {
sticker,
userFrom,
targetUser,
currentUserId: global.currentUserId,
};
},
)(GiftInfoModal));

View File

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

View File

@ -0,0 +1,15 @@
.root :global(.modal-content) {
padding: 0;
}
.root :global(.modal-dialog) {
max-width: 55vh;
}
.root :global(.modal-dialog), .root :global(.modal-content) {
overflow: hidden;
}
.picker {
height: 75vh;
}

View File

@ -0,0 +1,95 @@
import type { FC } from '../../../../lib/teact/teact';
import React, {
memo, useMemo, useState,
} from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global';
import {
filterUsersByName, isUserBot,
} from '../../../../global/helpers';
import { unique } from '../../../../util/iteratees';
import sortChatIds from '../../../common/helpers/sortChatIds';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import PeerPicker from '../../../common/pickers/PeerPicker';
import PickerModal from '../../../common/pickers/PickerModal';
import styles from './GiftRecipientPicker.module.scss';
export type OwnProps = {
modal?: boolean;
};
interface StateProps {
currentUserId?: string;
userSelectionLimit?: number;
userIds?: string[];
}
const GiftRecipientPicker: FC<OwnProps & StateProps> = ({
modal,
currentUserId,
userIds,
}) => {
const { closeGiftRecipientPicker, openGiftModal } = getActions();
const oldLang = useOldLang();
const isOpen = modal;
const [searchQuery, setSearchQuery] = useState<string>('');
const displayedUserIds = useMemo(() => {
const usersById = getGlobal().users.byId;
const filteredContactIds = userIds ? filterUsersByName(userIds, usersById, searchQuery) : [];
return sortChatIds(unique(filteredContactIds).filter((userId) => {
const user = usersById[userId];
if (!user) {
return true;
}
return !isUserBot(user) && userId !== currentUserId;
}));
}, [currentUserId, searchQuery, userIds]);
const handleSelectedUserIdsChange = useLastCallback((selectedId: string) => {
openGiftModal({ forUserId: selectedId });
closeGiftRecipientPicker();
});
return (
<PickerModal
className={styles.root}
isOpen={isOpen}
onClose={closeGiftRecipientPicker}
title={oldLang('GiftTelegramPremiumOrStarsTitle')}
hasCloseButton
shouldAdaptToSearch
withFixedHeight
>
<PeerPicker
className={styles.picker}
itemIds={displayedUserIds}
filterValue={searchQuery}
filterPlaceholder={oldLang('Search')}
onSelectedIdChange={handleSelectedUserIdsChange}
onFilterChange={setSearchQuery}
isSearchable
withDefaultPadding
withStatus
/>
</PickerModal>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const { currentUserId } = global;
return {
currentUserId,
userIds: global.contactList?.userIds,
userSelectionLimit: global.appConfig?.giveawayAddPeersMax,
};
})(GiftRecipientPicker));

View File

@ -7,6 +7,7 @@ import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
import type { TabState } from '../../../global/types';
import type { CustomPeer } from '../../../types';
import { STARS_ICON_PLACEHOLDER } from '../../../config';
import { getChatTitle, getUserFullName } from '../../../global/helpers';
import { selectChat, selectChatMessage, selectUser } from '../../../global/selectors';
import { formatInteger } from '../../../util/textFormat';
@ -165,7 +166,7 @@ const PaidReactionModal = ({
hasAbsoluteCloseButton
contentClassName={styles.content}
>
{starBalance !== undefined && <BalanceBlock balance={starBalance} className={styles.modalBalance} />}
<BalanceBlock balance={starBalance} className={styles.modalBalance} />
<StarSlider
className={styles.slider}
defaultValue={DEFAULT_STARS_AMOUNT}
@ -213,7 +214,7 @@ const PaidReactionModal = ({
{lang('SendPaidReaction', { amount: starsAmount }, {
withNodes: true,
specialReplacement: {
'⭐️': <Icon className={styles.buttonStar} name="star" />,
[STARS_ICON_PLACEHOLDER]: <Icon className={styles.buttonStar} name="star" />,
},
})}
</Button>

View File

@ -10,7 +10,7 @@ import StarIcon from '../../common/icons/StarIcon';
import styles from './StarsBalanceModal.module.scss';
type OwnProps = {
balance: number;
balance?: number;
className?: string;
};
@ -22,7 +22,7 @@ const BalanceBlock = ({ balance, className }: OwnProps) => {
<span className={styles.smallerText}>{lang('StarsBalance')}</span>
<div className={styles.balanceBottom}>
<StarIcon type="gold" size="middle" />
{formatInteger(balance)}
{balance !== undefined ? formatInteger(balance) : '…'}
</div>
</div>
);

View File

@ -39,8 +39,6 @@
@include mixins.adapt-padding-to-scrollbar(0.5rem);
@include mixins.side-panel-section;
border-bottom: 0;
}
.sectionTitle {
@ -51,11 +49,12 @@
padding: 0.25rem 0.75rem;
}
.secondaryInfo {
.tos {
font-size: 0.875rem;
color: var(--color-text-secondary);
background-color: var(--color-background-secondary);
padding: 0.5rem 1rem;
padding-top: 0;
}
.logo {

View File

@ -55,7 +55,7 @@ const StarsBalanceModal = ({
modal, starsBalanceState, canBuyPremium,
}: OwnProps & StateProps) => {
const {
closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingModal, openInvoice,
closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingPickerModal, openInvoice,
} = getActions();
const { balance, history, subscriptions } = starsBalanceState || {};
@ -69,9 +69,12 @@ const StarsBalanceModal = ({
const isOpen = Boolean(modal && starsBalanceState);
const { originPayment, originReaction } = modal || {};
const { originStarsPayment, originReaction, originGift } = modal || {};
const ongoingTransactionAmount = originPayment?.invoice?.amount || originReaction?.amount;
const ongoingTransactionAmount = originStarsPayment?.form?.invoice?.totalAmount
|| originStarsPayment?.subscriptionInfo?.subscriptionPricing?.amount
|| originReaction?.amount
|| originGift?.gift.stars;
const starsNeeded = ongoingTransactionAmount ? ongoingTransactionAmount - (balance || 0) : undefined;
const starsNeededText = useMemo(() => {
if (!starsNeeded || starsNeeded < 0) return undefined;
@ -83,17 +86,23 @@ const StarsBalanceModal = ({
return oldLang('StarsNeededTextReactions', getChatTitle(oldLang, channel));
}
if (originPayment) {
const bot = selectUser(global, originPayment.botId!);
if (originStarsPayment) {
const bot = originStarsPayment.form?.botId ? selectUser(global, originStarsPayment.form.botId) : undefined;
if (!bot) return undefined;
return oldLang('StarsNeededText', getUserFullName(bot));
}
return undefined;
}, [oldLang, originPayment, originReaction, starsNeeded]);
if (originGift) {
const user = selectUser(global, originGift.userId);
if (!user) return undefined;
return oldLang('StarsNeededTextGift', getUserFullName(user));
}
const shouldShowItems = Boolean(history?.all?.transactions.length && !originPayment && !originReaction);
const shouldSuggestGifting = !originPayment && !originReaction;
return undefined;
}, [starsNeeded, originReaction, originStarsPayment, originGift, oldLang]);
const shouldShowItems = Boolean(history?.all?.transactions.length && !originStarsPayment && !originReaction);
const shouldSuggestGifting = !originStarsPayment && !originReaction;
useEffect(() => {
if (!isOpen) {
@ -136,8 +145,8 @@ const StarsBalanceModal = ({
});
});
const openStarsGiftingModalHandler = useLastCallback(() => {
openStarsGiftingModal({});
const openStarsGiftingPickerModalHandler = useLastCallback(() => {
openStarsGiftingPickerModal({});
});
const handleBuyStars = useLastCallback((option: ApiStarTopupOption) => {
@ -163,7 +172,7 @@ const StarsBalanceModal = ({
>
<Icon name="close" />
</Button>
<BalanceBlock balance={balance || 0} className={styles.modalBalance} />
<BalanceBlock balance={balance} className={styles.modalBalance} />
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
<h2 className={styles.starHeaderText}>
{oldLang('TelegramStars')}
@ -193,7 +202,7 @@ const StarsBalanceModal = ({
<Button
className={buildClassName(styles.starButton, 'settings-main-menu-star')}
color="translucent"
onClick={openStarsGiftingModalHandler}
onClick={openStarsGiftingPickerModalHandler}
>
<StarIcon className="icon" type="gold" size="big" />
{oldLang('TelegramStarsGift')}
@ -207,9 +216,11 @@ const StarsBalanceModal = ({
/>
)}
</div>
<div className={styles.secondaryInfo}>
{tosText}
</div>
{areBuyOptionsShown && (
<div className={styles.tos}>
{tosText}
</div>
)}
{shouldShowItems && Boolean(subscriptions?.list.length) && (
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{oldLang('StarMySubscriptions')}</h3>

View File

@ -2,13 +2,13 @@ import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiChat, ApiChatInviteInfo, ApiMediaExtendedPreview, ApiMessage, ApiUser,
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiUser,
} from '../../../api/types';
import type { GlobalState, TabState } from '../../../global/types';
import { getChatTitle, getCustomPeerFromInvite, getUserFullName } from '../../../global/helpers';
import {
selectChat, selectChatMessage, selectTabState, selectUser,
selectChat, selectChatMessage, selectUser,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText';
@ -17,6 +17,7 @@ import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import usePrevious from '../../../hooks/usePrevious';
import Avatar from '../../common/Avatar';
import StarIcon from '../../common/icons/StarIcon';
@ -31,32 +32,34 @@ import styles from './StarsBalanceModal.module.scss';
import StarsBackground from '../../../assets/stars-bg.png';
export type OwnProps = {
modal: TabState['isStarPaymentModalOpen'];
modal: TabState['starsPayment'];
};
type StateProps = {
payment?: TabState['payment'];
starsBalanceState?: GlobalState['stars'];
bot?: ApiUser;
paidMediaMessage?: ApiMessage;
paidMediaChat?: ApiChat;
inviteInfo?: ApiChatInviteInfo;
};
const StarPaymentModal = ({
modal,
bot,
starsBalanceState,
payment,
paidMediaMessage,
paidMediaChat,
inviteInfo,
}: OwnProps & StateProps) => {
const { closePaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions();
const { closeStarsPaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions();
const [isLoading, markLoading, unmarkLoading] = useFlag();
const isOpen = Boolean(modal && starsBalanceState);
const isOpen = Boolean(modal?.inputInvoice && starsBalanceState);
const photo = payment?.invoice?.photo;
const prevModal = usePrevious(modal);
const renderingModal = modal || prevModal;
const { form, subscriptionInfo } = renderingModal || {};
const amount = form?.invoice?.totalAmount || subscriptionInfo?.subscriptionPricing?.amount;
const photo = form?.photo;
const oldLang = useOldLang();
const lang = useLang();
@ -68,12 +71,12 @@ const StarPaymentModal = ({
}, [isOpen]);
const descriptionText = useMemo(() => {
if (!payment?.invoice) {
if (!renderingModal?.inputInvoice) {
return '';
}
const botName = getUserFullName(bot);
const starsText = oldLang('Stars.Intro.PurchasedText.Stars', payment.invoice.amount);
const starsText = oldLang('Stars.Intro.PurchasedText.Stars', amount);
if (paidMediaMessage) {
const extendedMedia = paidMediaMessage.content.paidMedia!.extendedMedia as ApiMediaExtendedPreview[];
@ -88,22 +91,22 @@ const StarPaymentModal = ({
return oldLang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]);
}
if (inviteInfo) {
if (subscriptionInfo) {
return lang('StarsSubscribeText', {
chat: inviteInfo.title,
amount: payment.invoice.amount,
chat: subscriptionInfo.title,
amount: amount!,
}, {
withNodes: true,
withMarkdown: true,
pluralValue: payment.invoice.amount,
pluralValue: amount,
});
}
return oldLang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]);
}, [payment?.invoice, bot, oldLang, lang, paidMediaMessage, paidMediaChat, inviteInfo]);
return oldLang('Stars.Transfer.Info', [form!.title, botName, starsText]);
}, [renderingModal, bot, oldLang, amount, paidMediaMessage, subscriptionInfo, form, paidMediaChat, lang]);
const disclaimerText = useMemo(() => {
if (inviteInfo) {
if (subscriptionInfo) {
return lang('StarsSubscribeInfo', {
link: <SafeLink url={lang('StarsSubscribeInfoLink')} text={lang('StarsSubscribeInfoLinkText')} />,
}, {
@ -112,31 +115,30 @@ const StarPaymentModal = ({
}
return undefined;
}, [inviteInfo, lang]);
}, [subscriptionInfo, lang]);
const inviteCustomPeer = useMemo(() => {
if (!inviteInfo) {
if (!subscriptionInfo) {
return undefined;
}
return getCustomPeerFromInvite(inviteInfo);
}, [inviteInfo]);
return getCustomPeerFromInvite(subscriptionInfo);
}, [subscriptionInfo]);
const handlePayment = useLastCallback(() => {
const price = payment?.invoice?.amount;
const balance = starsBalanceState?.balance;
if (price === undefined || balance === undefined) {
if (amount === undefined || balance === undefined) {
return;
}
if (price > balance) {
if (amount > balance) {
openStarsBalanceModal({
originPayment: payment,
originStarsPayment: modal,
});
return;
}
sendStarPaymentForm();
sendStarPaymentForm({});
markLoading();
});
@ -146,9 +148,9 @@ const StarPaymentModal = ({
isOpen={isOpen}
hasAbsoluteCloseButton
isSlim
onClose={closePaymentModal}
onClose={closeStarsPaymentModal}
>
<BalanceBlock balance={starsBalanceState?.balance || 0} className={styles.modalBalance} />
<BalanceBlock balance={starsBalanceState?.balance} className={styles.modalBalance} />
<div className={styles.paymentImages} dir={oldLang.isRtl ? 'ltr' : 'rtl'}>
{paidMediaMessage ? (
<PaidMediaThumb media={paidMediaMessage.content.paidMedia!.extendedMedia} />
@ -174,7 +176,7 @@ const StarPaymentModal = ({
<Button className={styles.paymentButton} size="smaller" onClick={handlePayment} isLoading={isLoading}>
{oldLang('Stars.Transfer.Pay')}
<div className={styles.paymentAmount}>
{payment?.invoice?.amount}
{amount}
<StarIcon className={styles.paymentButtonStar} size="small" />
</div>
</Button>
@ -188,27 +190,20 @@ const StarPaymentModal = ({
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const payment = selectTabState(global).payment;
const bot = payment?.botId ? selectUser(global, payment.botId) : undefined;
(global, { modal }): StateProps => {
const bot = modal?.form?.botId ? selectUser(global, modal.form.botId) : undefined;
const messageInputInvoice = payment.inputInvoice?.type === 'message' ? payment.inputInvoice : undefined;
const messageInputInvoice = modal?.inputInvoice?.type === 'message' ? modal.inputInvoice : undefined;
const message = messageInputInvoice
? selectChatMessage(global, messageInputInvoice.chatId, messageInputInvoice.messageId) : undefined;
const chat = messageInputInvoice ? selectChat(global, messageInputInvoice.chatId) : undefined;
const isPaidMedia = message?.content.paidMedia;
const inviteInputInvoice = payment.inputInvoice?.type === 'chatInviteSubscription'
? payment.inputInvoice : undefined;
const inviteInfo = inviteInputInvoice?.inviteInfo;
return {
bot,
starsBalanceState: global.stars,
payment,
paidMediaMessage: isPaidMedia ? message : undefined,
paidMediaChat: isPaidMedia ? chat : undefined,
inviteInfo,
};
},
)(StarPaymentModal));

View File

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

View File

@ -1,15 +1,8 @@
@media (min-width: 451px) {
.modalDialog :global(.modal-dialog) {
max-width: 32rem !important;
}
}
.root :global(.modal-content) {
padding: 0;
}
.root :global(.modal-dialog) {
max-width: 26.25rem;
overflow: hidden;
}

View File

@ -1,60 +1,59 @@
import type { FC } from '../../../lib/teact/teact';
import type { FC } from '../../../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useRef,
useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
} from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global';
import type {
ApiStarTopupOption, ApiUser,
} from '../../../api/types';
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { getSenderTitle } from '../../../global/helpers';
import { getSenderTitle } from '../../../../global/helpers';
import {
selectTabState, selectUser,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatCurrencyAsString } from '../../../util/formatCurrency';
import renderText from '../../common/helpers/renderText';
selectUser,
} from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { formatCurrencyAsString } from '../../../../util/formatCurrency';
import renderText from '../../../common/helpers/renderText';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import SafeLink from '../../common/SafeLink';
import StarTopupOptionList from '../../modals/stars/StarTopupOptionList';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import Avatar from '../../../common/Avatar';
import SafeLink from '../../../common/SafeLink';
import Button from '../../../ui/Button';
import Modal from '../../../ui/Modal';
import StarTopupOptionList from '../StarTopupOptionList';
import styles from './StarsGiftModal.module.scss';
import StarLogo from '../../../assets/icons/StarLogo.svg';
import StarsBackground from '../../../assets/stars-bg.png';
import StarLogo from '../../../../assets/icons/StarLogo.svg';
import StarsBackground from '../../../../assets/stars-bg.png';
export type OwnProps = {
isOpen?: boolean;
modal: TabState['starsGiftModal'];
};
type StateProps = {
isCompleted?: boolean;
starsGiftOptions?: ApiStarTopupOption[] | undefined;
forUserId?: string;
user?: ApiUser;
};
const StarsGiftModal: FC<OwnProps & StateProps> = ({
isOpen,
isCompleted,
starsGiftOptions,
forUserId,
modal,
user,
}) => {
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
const {
closeStarsGiftModal, openInvoice, requestConfetti,
} = getActions();
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
const isOpen = Boolean(modal?.isOpen);
const renderingModal = useCurrentOrPrev(modal);
const oldLang = useOldLang();
@ -85,17 +84,19 @@ const StarsGiftModal: FC<OwnProps & StateProps> = ({
});
useEffect(() => {
if (isCompleted) {
if (renderingModal?.isCompleted) {
showConfetti();
}
}, [isCompleted, showConfetti]);
}, [renderingModal, showConfetti]);
const handleClick = useLastCallback((option: ApiStarTopupOption) => {
if (!renderingModal) return;
setSelectedOption(option);
if (user) {
openInvoice({
type: 'starsgift',
userId: forUserId!,
userId: user.id,
stars: option.stars,
currency: option.currency,
amount: option.amount,
@ -121,7 +122,7 @@ const StarsGiftModal: FC<OwnProps & StateProps> = ({
});
function renderGiftTitle() {
if (isCompleted) {
if (renderingModal?.isCompleted) {
return user ? renderText(oldLang('Notification.StarsGift.SentYou',
formatCurrencyAsString(selectedOption!.amount, selectedOption!.currency, oldLang.code)), ['simple_markdown'])
: renderText(oldLang('StarsAcquiredInfo', selectedOption?.stars), ['simple_markdown']);
@ -144,6 +145,7 @@ const StarsGiftModal: FC<OwnProps & StateProps> = ({
<Modal
className={buildClassName(styles.modalDialog, styles.root)}
dialogRef={dialogRef}
isSlim
onClose={handleClose}
isOpen={isOpen}
>
@ -190,12 +192,10 @@ const StarsGiftModal: FC<OwnProps & StateProps> = ({
) : oldLang('Stars.Purchase.GetStarsInfo')}
</p>
<div className={styles.section}>
{starsGiftOptions && (
<StarTopupOptionList
options={starsGiftOptions}
onClick={handleClick}
/>
)}
<StarTopupOptionList
options={renderingModal?.starsGiftOptions}
onClick={handleClick}
/>
<div className={styles.secondaryInfo}>
{bottomText}
</div>
@ -205,17 +205,10 @@ const StarsGiftModal: FC<OwnProps & StateProps> = ({
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
starsGiftOptions, forUserId, isCompleted,
} = selectTabState(global).starsGiftModal || {};
const user = forUserId ? selectUser(getGlobal(), forUserId) : undefined;
export default memo(withGlobal<OwnProps>((global, { modal }): StateProps => {
const user = modal?.forUserId ? selectUser(getGlobal(), modal.forUserId) : undefined;
return {
isCompleted,
starsGiftOptions,
forUserId,
user,
};
})(StarsGiftModal));

View File

@ -0,0 +1,23 @@
import type { ApiStarsTransaction } from '../../../../api/types';
import type { LangFn } from '../../../../hooks/useOldLang';
import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments';
export function getTransactionTitle(lang: LangFn, transaction: ApiStarsTransaction) {
if (transaction.extendedMedia) return lang('StarMediaPurchase');
if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase');
if (transaction.isReaction) return lang('StarsReactionsSent');
if (transaction.giveawayPostId) return lang('StarsGiveawayPrizeReceived');
if (transaction.isMyGift) return lang('StarsGiftSent');
if (transaction.isGift) return lang('StarsGiftReceived');
if (transaction.starGift) {
return transaction.stars < 0 ? lang('Gift2TransactionSent') : lang('Gift2ConvertedTitle');
}
const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined;
if (customPeer) return customPeer.title || lang(customPeer.titleKey!);
return transaction.title;
}

View File

@ -7,6 +7,7 @@ import type {
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import {
selectPeer,
} from '../../../../global/selectors';
@ -130,7 +131,7 @@ const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
}, {
withNodes: true,
specialReplacement: {
'⭐️': <StarIcon className={styles.amountStar} size="adaptive" type="gold" />,
[STARS_ICON_PLACEHOLDER]: <StarIcon className={styles.amountStar} size="adaptive" type="gold" />,
},
})}
</p>

View File

@ -14,6 +14,7 @@ import { selectPeer } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { CUSTOM_PEER_PREMIUM } from '../../../../util/objects/customPeer';
import { getTransactionTitle } from '../helpers/transaction';
import useSelector from '../../../../hooks/data/useSelector';
import useLastCallback from '../../../../hooks/useLastCallback';
@ -52,13 +53,7 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
const peer = useSelector(selectOptionalPeer(peerId));
const data = useMemo(() => {
let title = (() => {
if (transaction.extendedMedia) return lang('StarMediaPurchase');
if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase');
if (transaction.isReaction) return lang('StarsReactionsSent');
return transaction.title;
})();
let title = getTransactionTitle(lang, transaction);
let description;
let status: string | undefined;
let avatarPeer: ApiPeer | CustomPeer | undefined;

View File

@ -84,11 +84,13 @@
cursor: var(--custom-cursor, pointer);
}
.starTitle {
font-size: 1.5rem;
}
.subtitle {
text-align: center;
margin-top: 1rem;
}
.starGiftSticker {
height: 150px;
width: 150px;
position: relative;
}

View File

@ -4,24 +4,23 @@ import { getActions, withGlobal } from '../../../../global';
import type {
ApiPeer,
ApiStarsTransactionPeer, ApiSticker, ApiUser,
ApiStarsTransactionPeer, ApiSticker,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import { MediaViewerOrigin } from '../../../../types';
import { getMessageLink, getUserFullName } from '../../../../global/helpers';
import { getMessageLink } from '../../../../global/helpers';
import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../../global/helpers/payments';
import {
selectCanPlayAnimatedEmojis,
selectGiftStickerForStars,
selectPeer, selectUser,
selectPeer, selectStarGiftSticker,
} from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { copyTextToClipboard } from '../../../../util/clipboard';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import renderText from '../../../common/helpers/renderText';
import { getTransactionTitle } from '../helpers/transaction';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import usePrevious from '../../../../hooks/usePrevious';
@ -44,17 +43,15 @@ export type OwnProps = {
type StateProps = {
peer?: ApiPeer;
user?: ApiUser;
canPlayAnimatedEmojis?: boolean;
starGiftSticker?: ApiSticker;
topSticker?: ApiSticker;
};
const StarsTransactionModal: FC<OwnProps & StateProps> = ({
modal, peer, user, canPlayAnimatedEmojis, starGiftSticker,
modal, peer, canPlayAnimatedEmojis, topSticker,
}) => {
const { showNotification, openMediaViewer, closeStarsTransactionModal } = getActions();
const oldLang = useOldLang();
const lang = useLang();
const { transaction } = modal || {};
const handleOpenMedia = useLastCallback(() => {
@ -67,50 +64,14 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
});
});
const giftEntryAboutText = useMemo(() => {
const subtitleText = oldLang('lng_credits_box_history_entry_gift_in_about');
const subtitleTextParts = subtitleText.split('{link}');
return (
<>
{subtitleTextParts[0]}
<SafeLink
url={oldLang('lng_credits_box_history_entry_gift_about_url')}
text={oldLang('GiftStarsSubtitleLinkName')}
>
{renderText(oldLang('GiftStarsSubtitleLinkName'), ['simple_markdown'])}
</SafeLink>
{subtitleTextParts[1]}
</>
);
}, [oldLang]);
const giftOutAboutText = useMemo(() => {
return lang(
'CreditsBoxHistoryEntryGiftOutAbout',
{
user: <strong>{user ? getUserFullName(user) : ''}</strong>,
link: (
<SafeLink
url={oldLang('lng_credits_box_history_entry_gift_about_url')}
text={oldLang('GiftStarsSubtitleLinkName')}
>
{renderText(oldLang('GiftStarsSubtitleLinkName'), ['simple_markdown'])}
</SafeLink>
),
},
{
withNodes: true,
},
);
}, [lang, user, oldLang]);
const starModalData = useMemo(() => {
if (!transaction) {
return undefined;
}
const { isGift, isPrizeStars, photo } = transaction;
const {
giveawayPostId, photo,
} = transaction;
const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined;
@ -118,18 +79,11 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined;
const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer));
const title = (() => {
if (transaction.extendedMedia) return oldLang('StarMediaPurchase');
if (transaction.subscriptionPeriod) return oldLang('StarSubscriptionPurchase');
if (transaction.isReaction) return oldLang('StarsReactionsSent');
if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!);
return transaction.title;
})();
const title = getTransactionTitle(oldLang, transaction);
const messageLink = peer && transaction.messageId
? getMessageLink(peer, undefined, transaction.messageId) : undefined;
const giveawayMessageLink = peer && giveawayPostId && getMessageLink(peer, undefined, giveawayPostId);
const media = transaction.extendedMedia;
@ -143,7 +97,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const description = transaction.description || (media ? mediaText : undefined);
const shouldDisplayAvatar = !media && !isGift && !isPrizeStars;
const shouldDisplayAvatar = !media && !topSticker;
const avatarPeer = !photo ? (peer || customPeer) : undefined;
const header = (
@ -155,10 +109,10 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
onClick={handleOpenMedia}
/>
)}
{(isGift || isPrizeStars) && starGiftSticker && (
{!media && topSticker && (
<AnimatedIconFromSticker
key={transaction.id}
sticker={starGiftSticker}
sticker={topSticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
@ -174,12 +128,6 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
draggable={false}
/>
{title && <h1 className={styles.title}>{title}</h1>}
{(isGift || isPrizeStars) && (
<h1 className={buildClassName(styles.title, styles.starTitle)}>
{isPrizeStars ? oldLang('StarsGiveawayPrizeReceived')
: transaction?.isMyGift ? oldLang('StarsGiftSent') : oldLang('StarsGiftReceived')}
</h1>
)}
<p className={styles.description}>{description}</p>
<p className={styles.amount}>
<span className={buildClassName(styles.amount, transaction.stars < 0 ? styles.negative : styles.positive)}>
@ -187,11 +135,6 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
</span>
<StarIcon type="gold" size="middle" />
</p>
{isGift && (
<span className={styles.subtitle}>
{transaction?.isMyGift ? giftOutAboutText : giftEntryAboutText}
</span>
)}
</div>
);
@ -207,14 +150,12 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
tableData.push([oldLang('Stars.Transaction.Reaction.Post'), <SafeLink url={messageLink} text={messageLink} />]);
}
if (isPrizeStars) {
tableData.push(
[oldLang('BoostReason'), oldLang('Giveaway')],
[oldLang('Gift'), oldLang('Stars', transaction.stars, 'i')],
);
if (giveawayMessageLink) {
tableData.push([oldLang('BoostReason'), <SafeLink url={giveawayMessageLink} text={oldLang('Giveaway')} />]);
tableData.push([oldLang('Gift'), oldLang('Stars', transaction.stars, 'i')]);
}
if (transaction.id && !isPrizeStars) {
if (transaction.id) {
tableData.push([
oldLang('Stars.Transaction.Id'),
(
@ -257,9 +198,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
tableData,
footer,
};
}, [
transaction, oldLang, peer, giftOutAboutText, giftEntryAboutText, canPlayAnimatedEmojis, starGiftSticker,
]);
}, [transaction, oldLang, peer, topSticker, canPlayAnimatedEmojis]);
const prevModalData = usePrevious(starModalData);
const renderingModalData = prevModalData || starModalData;
@ -281,16 +220,17 @@ export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const peerId = modal?.transaction?.peer?.type === 'peer' && modal.transaction.peer.id;
const peer = peerId ? selectPeer(global, peerId) : undefined;
const user = peerId ? selectUser(global, peerId) : undefined;
const starCount = modal?.transaction.stars;
const starGiftSticker = selectGiftStickerForStars(global, starCount);
const starsGiftSticker = modal?.transaction.isGift && selectGiftStickerForStars(global, starCount);
const starGiftStickerId = modal?.transaction.starGift?.stickerId;
const starGiftSticker = starGiftStickerId && selectStarGiftSticker(global, starGiftStickerId);
return {
peer,
user,
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
starGiftSticker,
topSticker: starGiftSticker || starsGiftSticker,
};
},
)(StarsTransactionModal));

View File

@ -128,7 +128,7 @@
padding-left: 0.5rem;
padding-right: 0.5rem;
@include mixins.gradient-border-horizontal(0.5rem, calc(100% - 0.5rem));
@include mixins.gradient-border-horizontal(0.5rem, 0.5rem);
&::-webkit-scrollbar {
height: 0;

View File

@ -14,7 +14,7 @@ import type { WebAppOutboundEvent } from '../../../types/webapp';
import { getWebAppKey } from '../../../global/helpers/bots';
import {
selectCurrentChat, selectTabState, selectTheme, selectUser,
selectCurrentChat, selectTheme, selectUser,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
@ -56,8 +56,6 @@ type StateProps = {
bot?: ApiUser;
attachBot?: ApiAttachBot;
theme?: ThemeKey;
isPaymentModalOpen?: boolean;
paymentStatus?: TabState['payment']['status'];
};
const PROLONG_INTERVAL = 45000; // 45s
@ -588,16 +586,12 @@ export default memo(withGlobal<OwnProps>(
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
const chat = selectCurrentChat(global);
const theme = selectTheme(global);
const { isPaymentModalOpen, status } = selectTabState(global).payment;
const { isStarPaymentModalOpen } = selectTabState(global);
return {
attachBot,
bot,
chat,
theme,
isPaymentModalOpen: isPaymentModalOpen || isStarPaymentModalOpen,
paymentStatus: status,
};
},
)(WebAppModal));

View File

@ -112,6 +112,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
switchBotInline,
sharePhoneWithBot,
updateWebApp,
resetPaymentStatus,
} = getActions();
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
const [secondaryButton, setSecondaryButton] = useState<WebAppButton | undefined>();
@ -265,6 +266,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
setWebAppPaymentSlug({
slug: undefined,
});
resetPaymentStatus();
}
}, [isPaymentModalOpen, paymentStatus, sendEvent, webApp?.slug]);
@ -748,16 +750,18 @@ export default memo(withGlobal<OwnProps>(
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
const chat = selectCurrentChat(global);
const theme = selectTheme(global);
const { isPaymentModalOpen, status } = selectTabState(global).payment;
const { isStarPaymentModalOpen } = selectTabState(global);
const { isPaymentModalOpen, status: regularPaymentStatus } = selectTabState(global).payment;
const { status: starsPaymentStatus, inputInvoice: starsInputInvoice } = selectTabState(global).starsPayment;
const paymentStatus = starsPaymentStatus || regularPaymentStatus;
return {
attachBot,
bot,
chat,
theme,
isPaymentModalOpen: isPaymentModalOpen || isStarPaymentModalOpen,
paymentStatus: status,
isPaymentModalOpen: isPaymentModalOpen || Boolean(starsInputInvoice),
paymentStatus,
isMaximizedState,
};
},

View File

@ -108,8 +108,8 @@
border-radius: 1rem;
width: 1.5rem;
height: 1.5rem;
margin-inline-start: 0.125rem;
margin-inline-end: 1.25rem;
margin-inline-start: 0.0625rem;
margin-inline-end: 1.75rem;
}
.provider.stripe {

View File

@ -3,10 +3,13 @@ import React, { memo, useCallback } from '../../lib/teact/teact';
import { getActions } from '../../global';
import type {
ApiInvoice, ApiPaymentCredentials,
ApiInvoice,
ApiLabeledPrice,
ApiPaymentCredentials,
ApiWebDocument,
} from '../../api/types';
import type { FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
import type { LangCode, Price } from '../../types';
import type { LangCode } from '../../types';
import type { IconName } from '../../types/icons';
import { PaymentStep } from '../../types';
@ -26,7 +29,10 @@ import Skeleton from '../ui/placeholder/Skeleton';
import styles from './Checkout.module.scss';
export type OwnProps = {
invoice?: ApiInvoice;
title: string;
description: string;
photo?: ApiWebDocument;
invoice: ApiInvoice;
checkoutInfo?: {
paymentMethod?: string;
paymentProvider?: string;
@ -36,13 +42,11 @@ export type OwnProps = {
shippingMethod?: string;
botName?: string;
};
prices?: Price[];
totalPrice?: number;
needAddress?: boolean;
hasShippingOptions?: boolean;
tipAmount?: number;
shippingPrices?: Price[];
currency: string;
shippingPrices?: ApiLabeledPrice[];
isTosAccepted?: boolean;
dispatch?: FormEditDispatch;
onAcceptTos?: (isAccepted: boolean) => void;
@ -52,11 +56,12 @@ export type OwnProps = {
};
const Checkout: FC<OwnProps> = ({
title,
description,
photo,
invoice,
prices,
shippingPrices,
checkoutInfo,
currency,
totalPrice,
isTosAccepted,
dispatch,
@ -74,7 +79,7 @@ const Checkout: FC<OwnProps> = ({
const isInteractive = Boolean(dispatch);
const {
photo, title, text, termsUrl, suggestedTipAmounts, maxTipAmount,
termsUrl, suggestedTipAmounts, maxTipAmount,
} = invoice || {};
const {
paymentMethod,
@ -111,7 +116,7 @@ const Checkout: FC<OwnProps> = ({
{title}
</div>
<div>
{formatCurrency(tipAmount!, currency, lang.code)}
{formatCurrency(tipAmount!, invoice.currency, lang.code)}
</div>
</div>
<div className={styles.tipsList}>
@ -121,7 +126,7 @@ const Checkout: FC<OwnProps> = ({
className={buildClassName(styles.tipsItem, tip === tipAmount && styles.tipsItem_active)}
onClick={dispatch ? () => handleTipsClick(tip === tipAmount ? 0 : tip) : undefined}
>
{formatCurrency(tip, currency, lang.code, { shouldOmitFractions: true })}
{formatCurrency(tip, invoice.currency, lang.code, { shouldOmitFractions: true })}
</div>
))}
</div>
@ -172,19 +177,23 @@ const Checkout: FC<OwnProps> = ({
)}
<div className={styles.text}>
<h5 className={styles.checkoutTitle}>{title}</h5>
{text && <div className={styles.checkoutDescription}>{renderText(text, ['br', 'links', 'emoji'])}</div>}
{description && (
<div className={styles.checkoutDescription}>
{renderText(description, ['br', 'links', 'emoji'])}
</div>
)}
</div>
</div>
<div className={styles.priceInfo}>
{prices && prices.map((item) => (
renderPaymentItem(lang.code, item.label, item.amount, currency)
{invoice.prices.map((item) => (
renderPaymentItem(lang.code, item.label, item.amount, invoice.currency)
))}
{shippingPrices && shippingPrices.map((item) => (
renderPaymentItem(lang.code, item.label, item.amount, currency)
renderPaymentItem(lang.code, item.label, item.amount, invoice.currency)
))}
{suggestedTipAmounts && suggestedTipAmounts.length > 0 && renderTips()}
{totalPrice !== undefined && (
renderPaymentItem(lang.code, lang('Checkout.TotalAmount'), totalPrice, currency, true)
renderPaymentItem(lang.code, lang('Checkout.TotalAmount'), totalPrice, invoice.currency, true)
)}
</div>
<div className={styles.invoiceInfo}>
@ -262,7 +271,6 @@ function renderCheckoutItem({
return (
<ListItem
multiline={isMultiline}
narrow={isMultiline}
icon={icon}
inactive={!onClick}
onClick={onClick}

View File

@ -68,6 +68,6 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
return {
error: payment.error?.message,
passwordHint: global.twoFaSettings.hint,
savedCredentials: payment.savedCredentials,
savedCredentials: payment.form?.type === 'regular' ? payment.form.savedCredentials : undefined,
};
})(PasswordConfirm));

View File

@ -4,10 +4,12 @@ import React, {
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { ApiChat, ApiCountry, ApiPaymentCredentials } from '../../api/types';
import type {
ApiChat, ApiCountry, ApiInvoice, ApiLabeledPrice, ApiPaymentCredentials, ApiPaymentFormRegular,
} from '../../api/types';
import type { TabState } from '../../global/types';
import type { FormState } from '../../hooks/reducers/usePaymentReducer';
import type { Price, ShippingOption } from '../../types';
import type { ShippingOption } from '../../types';
import type { PaymentFormSubmitEvent } from './ConfirmPayment';
import { PaymentStep } from '../../types';
@ -49,15 +51,13 @@ export type OwnProps = {
};
type StateProps = {
step?: PaymentStep;
chat?: ApiChat;
isNameRequested?: boolean;
isShippingAddressRequested?: boolean;
isPhoneRequested?: boolean;
isEmailRequested?: boolean;
shouldSendPhoneToProvider?: boolean;
shouldSendEmailToProvider?: boolean;
currency?: string;
prices?: Price[];
nativeProvider?: string;
invoice?: ApiInvoice;
form?: ApiPaymentFormRegular;
error?: TabState['payment']['error'];
prices?: ApiLabeledPrice[];
isProviderError?: boolean;
needCardholderName?: boolean;
needCountry?: boolean;
@ -65,6 +65,7 @@ type StateProps = {
confirmPaymentUrl?: string;
countryList: ApiCountry[];
hasShippingOptions?: boolean;
shippingOptions?: ShippingOption[];
requestId?: string;
smartGlocalToken?: string;
stripeId?: string;
@ -75,32 +76,17 @@ type StateProps = {
botName?: string;
};
type GlobalStateProps = Pick<TabState['payment'], (
'step' | 'shippingOptions' |
'savedInfo' | 'canSaveCredentials' | 'nativeProvider' | 'passwordMissing' | 'invoice' | 'error'
)>;
const NETWORK_REQUEST_TIMEOUT_S = 3;
const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
const PaymentModal: FC<OwnProps & StateProps> = ({
isOpen,
onClose,
step,
shippingOptions,
savedInfo,
canSaveCredentials,
isNameRequested,
isShippingAddressRequested,
isPhoneRequested,
isEmailRequested,
shouldSendPhoneToProvider,
shouldSendEmailToProvider,
currency,
passwordMissing,
form,
isProviderError,
invoice,
nativeProvider,
prices,
needCardholderName,
needCountry,
needZip,
@ -189,10 +175,10 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
}, [error, paymentDispatch]);
useEffect(() => {
if (savedInfo) {
if (form?.savedInfo) {
const {
name: fullName, phone, email, shippingAddress,
} = savedInfo;
} = form.savedInfo;
const {
countryIso2, ...shippingAddressRest
} = shippingAddress || {};
@ -213,7 +199,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
},
});
}
}, [savedInfo, paymentDispatch, countryList]);
}, [form, paymentDispatch, countryList]);
useEffect(() => {
if (savedCredentials?.length) {
@ -233,8 +219,8 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
return 0;
}
return getTotalPrice(prices, shippingOptions, paymentState.shipping, paymentState.tipAmount);
}, [step, prices, shippingOptions, paymentState.shipping, paymentState.tipAmount]);
return getTotalPrice(invoice?.prices, shippingOptions, paymentState.shipping, paymentState.tipAmount);
}, [step, invoice?.prices, shippingOptions, paymentState.shipping, paymentState.tipAmount]);
const checkoutInfo = useMemo(() => {
if (step !== PaymentStep.Checkout) {
@ -295,19 +281,20 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
case PaymentStep.Checkout:
return (
<Checkout
prices={prices}
title={form!.title}
description={form!.description}
photo={form!.photo}
dispatch={paymentDispatch}
shippingPrices={paymentState.shipping && shippingOptions
? getShippingPrices(shippingOptions, paymentState.shipping)
: undefined}
totalPrice={totalPrice}
invoice={invoice}
invoice={invoice!}
checkoutInfo={checkoutInfo}
isPaymentFormUrl={isPaymentFormUrl}
currency={currency!}
hasShippingOptions={hasShippingOptions}
tipAmount={paymentState.tipAmount}
needAddress={Boolean(isShippingAddressRequested)}
needAddress={Boolean(invoice?.isShippingAddressRequested)}
savedCredentials={savedCredentials}
isTosAccepted={isTosAccepted}
onAcceptTos={setIsTosAccepted}
@ -337,7 +324,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
<PaymentInfo
state={paymentState}
dispatch={paymentDispatch}
canSaveCredentials={Boolean(!passwordMissing && canSaveCredentials)}
canSaveCredentials={Boolean(!form!.isPasswordMissing && form!.canSaveCredentials)}
needCardholderName={needCardholderName}
needCountry={needCountry}
needZip={needZip}
@ -349,10 +336,10 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
<ShippingInfo
state={paymentState}
dispatch={paymentDispatch}
needAddress={Boolean(isShippingAddressRequested)}
needEmail={Boolean(isEmailRequested || shouldSendEmailToProvider)}
needPhone={Boolean(isPhoneRequested || shouldSendPhoneToProvider)}
needName={Boolean(isNameRequested)}
needAddress={Boolean(invoice?.isShippingAddressRequested)}
needEmail={Boolean(invoice?.isEmailRequested || invoice?.isEmailSentToProvider)}
needPhone={Boolean(invoice?.isPhoneRequested || invoice?.isPhoneSentToProvider)}
needName={Boolean(invoice?.isNameRequested)}
countryList={countryList!}
/>
);
@ -362,7 +349,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
state={paymentState}
dispatch={paymentDispatch}
shippingOptions={shippingOptions || []}
currency={currency!}
currency={invoice!.currency}
/>
);
case PaymentStep.ConfirmPayment:
@ -429,7 +416,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
return;
}
if (savedInfo && !requestId && !paymentState.shipping) {
if (form?.savedInfo && !requestId && !paymentState.shipping) {
setIsLoading(true);
validateRequest();
return;
@ -455,16 +442,16 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
}
const { phone, email, fullName } = paymentState;
const shouldFillRequestedData = (isEmailRequested && !email)
|| (isPhoneRequested && !phone)
|| (isNameRequested && !fullName);
const shouldFillRequestedData = (invoice?.isEmailRequested && !email)
|| (invoice?.isPhoneRequested && !phone)
|| (invoice?.isNameRequested && !fullName);
if ((isShippingAddressRequested && !requestId) || shouldFillRequestedData) {
if ((invoice?.isShippingAddressRequested && !requestId) || shouldFillRequestedData) {
setStep(PaymentStep.ShippingInfo);
return;
}
if (isShippingAddressRequested && !paymentState.shipping && shippingOptions?.length) {
if (invoice?.isShippingAddressRequested && !paymentState.shipping && shippingOptions?.length) {
setStep(PaymentStep.Shipping);
return;
}
@ -517,7 +504,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
}, [step, lang]);
const buttonText = step === PaymentStep.Checkout
? lang('Checkout.PayPrice', formatCurrencyAsString(totalPrice, currency!, lang.code))
? lang('Checkout.PayPrice', formatCurrencyAsString(totalPrice, invoice!.currency, lang.code))
: lang('Next');
function getIsSubmitDisabled() {
@ -625,41 +612,27 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(global): StateProps & GlobalStateProps => {
(global): StateProps => {
const {
form,
step,
shippingOptions,
savedInfo,
canSaveCredentials,
invoice,
invoiceContainer,
nativeProvider,
nativeParams,
passwordMissing,
error,
confirmPaymentUrl,
inputInvoice,
requestId,
stripeCredentials,
smartGlocalCredentials,
savedCredentials,
temporaryPassword,
isExtendedMedia,
url,
botId,
type,
} = selectTabState(global).payment;
const { invoice, nativeParams, nativeProvider } = form || {};
const countryList = global.countryList.general;
// Handled in `StarPaymentModal`
if (type === 'stars') {
return {
countryList,
};
}
let providerName = nativeProvider;
let providerName = form?.nativeProvider;
if (!providerName && url) {
providerName = url.startsWith(DONATE_PROVIDER_URL) ? DONATE_PROVIDER : undefined;
}
@ -667,16 +640,6 @@ export default memo(withGlobal<OwnProps>(
const chat = inputInvoice && 'chatId' in inputInvoice ? selectChat(global, inputInvoice.chatId!) : undefined;
const isProviderError = Boolean(invoice && (!providerName || !SUPPORTED_PROVIDERS.has(providerName)));
const { needCardholderName, needCountry, needZip } = (nativeParams || {});
const {
isNameRequested,
isShippingAddressRequested,
isPhoneRequested,
isEmailRequested,
shouldSendPhoneToProvider,
shouldSendEmailToProvider,
currency,
prices,
} = (invoiceContainer || {});
const bot = botId ? selectUser(global, botId) : undefined;
const botName = getUserFullName(bot);
@ -684,19 +647,9 @@ export default memo(withGlobal<OwnProps>(
step,
chat,
shippingOptions,
savedInfo,
canSaveCredentials,
nativeProvider: providerName,
passwordMissing,
isNameRequested,
isShippingAddressRequested,
isPhoneRequested,
isEmailRequested,
shouldSendPhoneToProvider,
shouldSendEmailToProvider,
currency,
prices,
isProviderError,
form,
invoice,
needCardholderName,
needCountry,
@ -709,7 +662,6 @@ export default memo(withGlobal<OwnProps>(
hasShippingOptions: Boolean(shippingOptions?.length),
smartGlocalToken: smartGlocalCredentials?.token,
stripeId: stripeCredentials?.id,
savedCredentials,
passwordValidUntil: temporaryPassword?.validUntil,
isExtendedMedia,
botName,
@ -727,7 +679,7 @@ function getShippingPrices(shippingOptions: ShippingOption[], shippingOption: st
}
function getTotalPrice(
prices: Price[] = [],
prices: ApiLabeledPrice[] = [],
shippingOptions: ShippingOption[] | undefined,
shippingOption: string,
tipAmount: number,

View File

@ -2,13 +2,13 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ApiInvoice, ApiShippingAddress, ApiWebDocument } from '../../api/types';
import type { Price } from '../../types';
import type { ApiReceiptRegular, ApiShippingAddress } from '../../api/types';
import { selectTabState } from '../../global/selectors';
import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
import usePrevious from '../../hooks/usePrevious';
import Button from '../ui/Button';
import Modal from '../ui/Modal';
@ -22,37 +22,13 @@ export type OwnProps = {
};
type StateProps = {
prices?: Price[];
shippingPrices: any;
tipAmount?: number;
totalAmount?: number;
currency?: string;
info?: {
shippingAddress?: ApiShippingAddress;
phone?: string;
name?: string;
};
photo?: ApiWebDocument;
text?: string;
title?: string;
credentialsTitle?: string;
shippingMethod?: string;
receipt?: ApiReceiptRegular;
};
const ReceiptModal: FC<OwnProps & StateProps> = ({
isOpen,
onClose,
prices,
shippingPrices,
tipAmount,
totalAmount,
currency,
info,
photo,
text,
title,
credentialsTitle,
shippingMethod,
receipt,
}) => {
const lang = useLang();
@ -64,20 +40,13 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
}
}, [isOpen, openModal]);
const checkoutInfo = useMemo(() => {
return getCheckoutInfo(credentialsTitle, info, shippingMethod);
}, [info, shippingMethod, credentialsTitle]);
const prevReceipt = usePrevious(receipt);
const renderingReceipt = receipt || prevReceipt;
const invoice: ApiInvoice = useMemo(() => {
return {
mediaType: 'invoice',
photo,
text: text!,
title: title!,
amount: totalAmount!,
currency: currency!,
};
}, [currency, photo, text, title, totalAmount]);
const checkoutInfo = useMemo(() => {
if (!renderingReceipt) return undefined;
return getCheckoutInfo(renderingReceipt.credentialsTitle, renderingReceipt.info, renderingReceipt.shippingMethod);
}, [renderingReceipt]);
return (
<Modal
@ -86,32 +55,35 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
onClose={closeModal}
onCloseAnimationEnd={onClose}
>
<div>
<div className="header" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
className="close-button"
color="translucent"
round
size="smaller"
onClick={closeModal}
ariaLabel="Close"
>
<i className="icon icon-close" />
</Button>
<h3> {lang('PaymentReceipt')} </h3>
</div>
<div className="receipt-content custom-scroll">
<Checkout
prices={prices}
shippingPrices={shippingPrices}
totalPrice={totalAmount}
tipAmount={tipAmount}
invoice={invoice}
checkoutInfo={checkoutInfo}
currency={currency!}
/>
</div>
</div>
{renderingReceipt && (
<>
<div className="header" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
className="close-button"
color="translucent"
round
size="smaller"
onClick={closeModal}
ariaLabel="Close"
>
<i className="icon icon-close" />
</Button>
<h3> {lang('PaymentReceipt')} </h3>
</div>
<div className="receipt-content custom-scroll">
<Checkout
shippingPrices={renderingReceipt.shippingPrices}
totalPrice={renderingReceipt.totalAmount}
tipAmount={renderingReceipt.tipAmount}
invoice={renderingReceipt.invoice}
checkoutInfo={checkoutInfo}
title={renderingReceipt.title}
description={renderingReceipt.description}
photo={renderingReceipt.photo}
/>
</div>
</>
)}
</Modal>
);
};
@ -119,39 +91,16 @@ const ReceiptModal: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { receipt } = selectTabState(global).payment;
const {
currency,
prices,
info,
totalAmount,
credentialsTitle,
shippingPrices,
shippingMethod,
photo,
text,
title,
tipAmount,
} = (receipt || {});
return {
currency,
prices,
info,
tipAmount,
totalAmount,
credentialsTitle,
shippingPrices,
shippingMethod,
photo,
text,
title,
receipt,
};
},
)(ReceiptModal));
function getCheckoutInfo(paymentMethod?: string,
info?:
{ phone?: string;
info?: {
phone?: string;
name?: string;
shippingAddress?: ApiShippingAddress;
},

View File

@ -72,11 +72,16 @@
&.storiesArchive-list,
&.stories-list,
&.media-list,
&.previewMedia-list {
&.previewMedia-list,
&.gifts-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
grid-gap: 0.0625rem;
gap: 0.0625rem;
}
&.gifts-list {
gap: 0.625rem;
}
&.documents-list {
@ -117,7 +122,8 @@
&.similarChannels-list,
&.commonChats-list,
&.members-list {
&.members-list,
&.gifts-list {
padding: 0.5rem;
@media (max-width: 600px) {

View File

@ -12,6 +12,7 @@ import type {
ApiMessage,
ApiTypeStory,
ApiUser,
ApiUserStarGift,
ApiUserStatus,
} from '../../api/types';
import type { TabState } from '../../global/types';
@ -76,6 +77,7 @@ import useTransitionFixes from './hooks/useTransitionFixes';
import Audio from '../common/Audio';
import Document from '../common/Document';
import UserGift from '../common/gift/UserGift';
import GroupChatInfo from '../common/GroupChatInfo';
import Media from '../common/Media';
import NothingFound from '../common/NothingFound';
@ -116,6 +118,8 @@ type StateProps = {
hasStoriesTab?: boolean;
hasMembersTab?: boolean;
hasPreviewMediaTab?: boolean;
hasGiftsTab?: boolean;
gifts?: ApiUserStarGift[];
areMembersHidden?: boolean;
canAddMembers?: boolean;
canDeleteMembers?: boolean;
@ -178,6 +182,8 @@ const Profile: FC<OwnProps & StateProps> = ({
hasStoriesTab,
hasMembersTab,
hasPreviewMediaTab,
hasGiftsTab,
gifts,
botPreviewMedia,
areMembersHidden,
canAddMembers,
@ -218,6 +224,7 @@ const Profile: FC<OwnProps & StateProps> = ({
openPremiumModal,
loadChannelRecommendations,
loadPreviewMedias,
loadUserGifts,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -234,6 +241,7 @@ const Profile: FC<OwnProps & StateProps> = ({
...(isSavedMessages && !isSavedDialog ? [{ type: 'dialogs' as const, title: 'SavedDialogsTab' }] : []),
...(hasStoriesTab ? [{ type: 'stories' as const, title: 'ProfileStories' }] : []),
...(hasStoriesTab && isSavedMessages ? [{ type: 'storiesArchive' as const, title: 'ProfileStoriesArchive' }] : []),
...(hasGiftsTab ? [{ type: 'gifts' as const, title: 'ProfileGifts' }] : []),
...(hasMembersTab ? [{
type: 'members' as const, title: isChannel ? 'ChannelSubscribers' : 'GroupMembers',
}] : []),
@ -253,6 +261,7 @@ const Profile: FC<OwnProps & StateProps> = ({
hasMembersTab,
hasPreviewMediaTab,
hasStoriesTab,
hasGiftsTab,
isChannel,
isTopicInfo,
similarChannels,
@ -298,6 +307,10 @@ const Profile: FC<OwnProps & StateProps> = ({
}
}, [chatId, isChannel, similarChannels, isSynced]);
const giftIds = useMemo(() => {
return gifts?.map(({ date, gift, fromId }) => `${date}-${fromId}-${gift.id}`);
}, [gifts]);
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
const tabType = tabs[renderingActiveTab].type as ProfileTabType;
const handleLoadCommonChats = useCallback(() => {
@ -309,28 +322,33 @@ const Profile: FC<OwnProps & StateProps> = ({
const handleLoadStoriesArchive = useCallback(({ offsetId }: { offsetId: number }) => {
loadStoriesArchive({ peerId: currentUserId!, offsetId });
}, [currentUserId]);
const handleLoadGifts = useCallback(() => {
loadUserGifts({ userId: chatId });
}, [chatId]);
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds(
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds({
loadMoreMembers,
handleLoadCommonChats,
searchSharedMediaMessages,
handleLoadPeerStories,
handleLoadStoriesArchive,
searchMessages: searchSharedMediaMessages,
loadStories: handleLoadPeerStories,
loadStoriesArchive: handleLoadStoriesArchive,
loadMoreGifts: handleLoadGifts,
loadCommonChats: handleLoadCommonChats,
tabType,
mediaSearchType,
members,
groupChatMembers: members,
commonChatIds,
usersById,
userStatusesById,
chatsById,
messagesById,
chatMessages: messagesById,
foundIds,
threadId,
storyIds,
giftIds,
pinnedStoryIds,
archiveStoryIds,
similarChannels,
);
});
const isFirstTab = (isSavedMessages && resultType === 'dialogs')
|| (hasStoriesTab && resultType === 'stories')
|| resultType === 'members'
@ -670,6 +688,10 @@ const Profile: FC<OwnProps & StateProps> = ({
</>
)}
</div>
) : resultType === 'gifts' ? (
(gifts?.map((gift) => (
<UserGift userId={chatId} key={`${gift.date}-${gift.fromId}-${gift.gift.id}`} gift={gift} />
)))
) : undefined}
</div>
);
@ -795,6 +817,9 @@ export default memo(withGlobal<OwnProps>(
const storyByIds = peerStories?.byId;
const archiveStoryIds = peerStories?.archiveIds;
const hasGiftsTab = Boolean(userFullInfo?.starGiftCount);
const userGifts = global.users.giftsById[chatId];
return {
theme: selectTheme(global),
isChannel,
@ -816,6 +841,8 @@ export default memo(withGlobal<OwnProps>(
userStatusesById,
chatsById,
storyIds,
hasGiftsTab,
gifts: userGifts?.gifts,
pinnedStoryIds,
archiveStoryIds,
storyByIds,

View File

@ -12,27 +12,51 @@ import sortChatIds from '../../common/helpers/sortChatIds';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useSyncEffect from '../../../hooks/useSyncEffect';
export default function useProfileViewportIds(
loadMoreMembers: AnyToVoidFunction,
loadCommonChats: AnyToVoidFunction,
searchMessages: AnyToVoidFunction,
loadStories: AnyToVoidFunction,
loadStoriesArchive: AnyToVoidFunction,
tabType: ProfileTabType,
mediaSearchType?: SharedMediaType,
groupChatMembers?: ApiChatMember[],
commonChatIds?: string[],
usersById?: Record<string, ApiUser>,
userStatusesById?: Record<string, ApiUserStatus>,
chatsById?: Record<string, ApiChat>,
chatMessages?: Record<number, ApiMessage>,
foundIds?: number[],
threadId?: ThreadId,
storyIds?: number[],
pinnedStoryIds?: number[],
archiveStoryIds?: number[],
similarChannels?: string[],
) {
export default function useProfileViewportIds({
loadMoreMembers,
loadCommonChats,
searchMessages,
loadStories,
loadStoriesArchive,
loadMoreGifts,
tabType,
mediaSearchType,
groupChatMembers,
commonChatIds,
usersById,
userStatusesById,
chatsById,
chatMessages,
foundIds,
threadId,
storyIds,
giftIds,
pinnedStoryIds,
archiveStoryIds,
similarChannels,
} : {
loadMoreMembers: AnyToVoidFunction;
loadCommonChats: AnyToVoidFunction;
searchMessages: AnyToVoidFunction;
loadStories: AnyToVoidFunction;
loadStoriesArchive: AnyToVoidFunction;
loadMoreGifts: AnyToVoidFunction;
tabType: ProfileTabType;
mediaSearchType?: SharedMediaType;
groupChatMembers?: ApiChatMember[];
commonChatIds?: string[];
usersById?: Record<string, ApiUser>;
userStatusesById?: Record<string, ApiUserStatus>;
chatsById?: Record<string, ApiChat>;
chatMessages?: Record<number, ApiMessage>;
foundIds?: number[];
threadId?: ThreadId;
storyIds?: number[];
giftIds?: string[];
pinnedStoryIds?: number[];
archiveStoryIds?: number[];
similarChannels?: string[];
}) {
const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType;
const memberIds = useMemo(() => {
@ -160,6 +184,10 @@ export default function useProfileViewportIds(
case 'similarChannels':
viewportIds = similarChannels;
break;
case 'gifts':
viewportIds = giftIds;
getMore = loadMoreGifts;
break;
case 'dialogs':
noProfileInfo = true;
break;

View File

@ -280,6 +280,20 @@
}
}
&.stars {
background-color: #FFB727;
color: var(--color-white);
--ripple-color: rgba(0, 0, 0, 0.08);
@include active-styles() {
background-color: #FFB727CC;
}
@include no-ripple-styles() {
background-color: #FFB727;
}
}
&.smaller {
height: 2.75rem;
padding: 0.3125rem;
@ -341,7 +355,7 @@
}
&.pill {
height: 2rem;
height: 1.875rem;
border-radius: 1rem;
padding: 0.3125rem 1rem;
font-size: 1rem;
@ -366,6 +380,10 @@
padding-left: 1.375rem;
padding-right: 1.375rem;
}
&.pill {
padding: 0.5rem 0.75rem;
}
}
&.pill {

View File

@ -7,7 +7,9 @@ import buildStyle from '../../util/buildStyle';
import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import Sparkles from '../common/Sparkles';
import RippleEffect from './RippleEffect';
import Spinner from './Spinner';
@ -20,7 +22,7 @@ export type OwnProps = {
size?: 'default' | 'smaller' | 'tiny';
color?: (
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black'
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive'
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'sparkles'
);
backgroundImage?: string;
id?: string;
@ -46,6 +48,7 @@ export type OwnProps = {
isShiny?: boolean;
isRectangular?: boolean;
withPremiumGradient?: boolean;
withSparkleEffect?: boolean;
noPreventDefault?: boolean;
noForcedUpperCase?: boolean;
shouldStopPropagation?: boolean;
@ -86,6 +89,7 @@ const Button: FC<OwnProps> = ({
isLoading,
isShiny,
withPremiumGradient,
withSparkleEffect,
onTransitionEnd,
ariaLabel,
ariaControls,
@ -112,6 +116,8 @@ const Button: FC<OwnProps> = ({
elementRef = ref;
}
const lang = useOldLang();
const [isClicked, setIsClicked] = useState(false);
const isNotInteractive = disabled || nonInteractive;
@ -164,6 +170,21 @@ const Button: FC<OwnProps> = ({
}
});
const content = (
<>
{color === 'sparkles' && withSparkleEffect && <Sparkles preset="button" />}
{isLoading ? (
<div>
<span dir={isRtl ? 'auto' : undefined}>{lang('Cache.ClearProgress')}</span>
<Spinner color={isText ? 'blue' : 'white'} />
</div>
) : children}
{!isNotInteractive && ripple && (
<RippleEffect />
)}
</>
);
if (href) {
return (
<a
@ -182,10 +203,7 @@ const Button: FC<OwnProps> = ({
target="_blank"
rel="noreferrer"
>
{children}
{!isNotInteractive && ripple && (
<RippleEffect />
)}
{content}
</a>
);
}
@ -212,15 +230,7 @@ const Button: FC<OwnProps> = ({
dir={isRtl ? 'rtl' : undefined}
style={buildStyle(style, backgroundImage && `background-image: url(${backgroundImage})`) || undefined}
>
{isLoading ? (
<div>
<span dir={isRtl ? 'auto' : undefined}>Please wait...</span>
<Spinner color={isText ? 'blue' : 'white'} />
</div>
) : children}
{!isNotInteractive && ripple && (
<RippleEffect />
)}
{content}
</button>
);
};

View File

@ -12,6 +12,7 @@ import './api/bots';
import './api/settings';
import './api/twoFaSettings';
import './api/payments';
import './api/stars';
import './api/reactions';
import './api/statistics';
import './api/stories';
@ -28,6 +29,7 @@ import './ui/payments';
import './ui/calls';
import './ui/mediaViewer';
import './ui/passcode';
import './ui/stars';
import './ui/reactions';
import './ui/stories';
import './apiUpdaters/initial';

View File

@ -25,7 +25,6 @@ import {
RE_TG_LINK,
SAVED_FOLDER_ID,
SERVICE_NOTIFICATIONS_USER_ID,
STARS_CURRENCY_CODE,
TME_WEB_DOMAINS,
TMP_CHAT_ID,
TOP_CHAT_MESSAGES_PRELOAD_LIMIT,
@ -70,6 +69,7 @@ import {
replaceChatFullInfo,
replaceChatListIds,
replaceChatListLoadingParameters,
replaceMessages,
replaceThreadParam,
replaceUserStatuses,
toggleSimilarChannels,
@ -499,7 +499,7 @@ addActionHandler('openSupportChat', async (global, actions, payload): Promise<vo
});
addActionHandler('loadAllChats', async (global, actions, payload): Promise<void> => {
const { onFirstBatchDone } = payload;
const { whenFirstBatchDone } = payload;
const listType = payload.listType;
let isCallbackFired = false;
let i = 0;
@ -526,7 +526,7 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
);
if (!isCallbackFired) {
onFirstBatchDone?.();
await whenFirstBatchDone?.();
isCallbackFired = true;
}
@ -1215,24 +1215,14 @@ addActionHandler('checkChatInvite', async (global, actions, payload): Promise<vo
if (result.invite.subscriptionFormId) {
global = updateTabState(global, {
payment: {
formId: result.invite.subscriptionFormId,
starsPayment: {
inputInvoice: {
type: 'chatInviteSubscription',
hash,
inviteInfo: result.invite,
},
invoice: {
amount: result.invite.subscriptionPricing!.amount,
currency: STARS_CURRENCY_CODE,
isRecurring: true,
mediaType: 'invoice',
// Placeholder values
title: 'Subscription',
text: '',
},
subscriptionInfo: result.invite,
status: 'pending',
},
isStarPaymentModalOpen: true,
}, tabId);
setGlobal(global);
return;
@ -2783,7 +2773,7 @@ async function loadChats(
}
global = updateChatListSecondaryInfo(global, listType, result);
global = addMessages(global, result.messages);
global = replaceMessages(global, result.messages);
global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType);
if (!shouldIgnorePagination) {

View File

@ -1,6 +1,8 @@
import type { ApiInputInvoiceStars, ApiRequestInputInvoice } from '../../../api/types';
import type { ApiInputInvoiceStarGift, ApiRequestInputInvoice } from '../../../api/types';
import type { ApiCredentials } from '../../../components/payment/PaymentModal';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import type {
ActionReturnType, GlobalState, TabArgs,
} from '../../types';
import { PaymentStep } from '../../../types';
import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config';
@ -12,36 +14,32 @@ import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs';
import { isChatChannel, isChatSuperGroup } from '../../helpers';
import {
getPrizeStarsTransactionFromGiveaway,
getRequestInputInvoice,
} from '../../helpers/payments';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
appendStarsSubscriptions,
appendStarsTransactions, closeInvoice,
closeInvoice,
openStarsTransactionFromReceipt,
openStarsTransactionModal,
setInvoiceInfo, setPaymentForm,
setPaymentStep,
setReceipt,
setRequestInfoId,
setSmartGlocalCardInfo, setStripeCardInfo,
setSmartGlocalCardInfo,
setStripeCardInfo,
updateChatFullInfo,
updatePayment,
updateShippingOptions,
updateStarsBalance,
updateStarsPayment,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
selectChat,
selectChatFullInfo,
selectChatMessage,
selectPaymentFormId,
selectPaymentInputInvoice, selectPaymentRequestId,
selectPeer,
selectPaymentInputInvoice,
selectPaymentRequestId,
selectProviderPublicToken,
selectProviderPublishableKey,
selectSmartGlocalCredentials,
selectStarsPayment,
selectStripeCredentials,
selectTabState,
} from '../../selectors';
@ -72,53 +70,110 @@ addActionHandler('openInvoice', async (global, actions, payload): Promise<void>
return;
}
const result = await getPaymentForm(global, requestInputInvoice, tabId);
global = updateTabState(global, {
isPaymentFormLoading: true,
}, tabId);
setGlobal(global);
if (!result) {
const theme = extractCurrentThemeParams();
const form = await callApi('getPaymentForm', requestInputInvoice, theme);
if (!form) {
return;
}
const { form, invoice } = result;
global = getGlobal();
global = setInvoiceInfo(global, invoice, tabId);
global = updatePayment(global, {
inputInvoice: payload,
isPaymentModalOpen: form.type === 'regular',
isExtendedMedia: (payload as any).isExtendedMedia,
status: undefined,
global = updateTabState(global, {
isPaymentFormLoading: false,
}, tabId);
if ('error' in form) {
setGlobal(global);
return;
}
if (form.type === 'regular') {
global = updatePayment(global, {
inputInvoice: payload,
form,
isPaymentModalOpen: true,
isExtendedMedia: (payload as any).isExtendedMedia,
status: undefined,
}, tabId);
global = setPaymentStep(global, PaymentStep.Checkout, tabId);
}
if (form.type === 'stars') {
global = updateTabState(global, {
isStarPaymentModalOpen: true,
starsPayment: {
inputInvoice,
form,
status: 'pending',
},
}, tabId);
}
setGlobal(global);
});
async function getPaymentForm<T extends GlobalState>(
global: T, inputInvoice: ApiRequestInputInvoice,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const theme = extractCurrentThemeParams();
const result = await callApi('getPaymentForm', inputInvoice, theme);
if (!result) {
return undefined;
addActionHandler('sendStarGift', async (global, actions, payload): Promise<void> => {
const {
gift, userId, message, shouldHideName, tabId = getCurrentTabId(),
} = payload;
const balance = global.stars?.balance;
if (balance === undefined) return;
if (balance < gift.stars) {
actions.openStarsBalanceModal({ tabId });
return;
}
const {
form, invoice,
} = result;
const inputInvoice: ApiInputInvoiceStarGift = {
type: 'stargift',
userId,
giftId: gift.id,
message,
shouldHideName,
};
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 = setPaymentForm(global, form, tabId);
global = setPaymentStep(global, PaymentStep.Checkout, tabId);
global = updateTabState(global, {
isPaymentFormLoading: false,
}, tabId);
setGlobal(global);
return { form, invoice };
}
if ('error' in form) {
return;
}
actions.sendStarPaymentForm({
starGift: {
inputInvoice,
formId: form.formId,
},
tabId,
});
});
addActionHandler('getReceipt', async (global, actions, payload): Promise<void> => {
const {
@ -167,7 +222,7 @@ addActionHandler('clearReceipt', (global, actions, payload): ActionReturnType =>
addActionHandler('sendCredentialsInfo', (global, actions, payload): ActionReturnType => {
const { credentials, tabId = getCurrentTabId() } = payload;
const { nativeProvider } = selectTabState(global, tabId).payment;
const { nativeProvider } = selectTabState(global, tabId).payment.form!;
const { data } = credentials;
if (nativeProvider === 'stripe') {
@ -190,15 +245,16 @@ addActionHandler('sendPaymentForm', async (global, actions, payload): Promise<vo
shippingOptionId, saveCredentials, savedCredentialId, tipAmount,
tabId = getCurrentTabId(),
} = payload;
const inputInvoice = selectPaymentInputInvoice(global, tabId);
const formId = selectPaymentFormId(global, tabId);
const requestInfoId = selectPaymentRequestId(global, tabId);
const { nativeProvider, temporaryPassword } = selectTabState(global, tabId).payment;
const paymentState = selectTabState(global, tabId).payment;
const { form, temporaryPassword, inputInvoice } = paymentState;
if (!inputInvoice || !formId) {
if (!inputInvoice || !form) {
return;
}
const { nativeProvider, formId } = form;
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
if (!requestInputInvoice) {
return;
@ -234,51 +290,55 @@ addActionHandler('sendPaymentForm', async (global, actions, payload): Promise<vo
actions.apiUpdate({
'@type': 'updatePaymentStateCompleted',
inputInvoice,
paymentState,
tabId,
});
if (inputInvoice.type === 'stars') {
actions.requestConfetti({ withStars: true, tabId });
}
});
addActionHandler('sendStarPaymentForm', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const starsPayment = selectTabState(global, tabId).isStarPaymentModalOpen;
if (!starsPayment) return;
const inputInvoice = selectPaymentInputInvoice(global, tabId) as ApiInputInvoiceStars;
const formId = selectPaymentFormId(global, tabId);
if (!inputInvoice || !formId) {
return;
}
const { starGift, tabId = getCurrentTabId() } = payload;
const starPayment = selectStarsPayment(global, tabId);
const inputInvoice = starPayment?.inputInvoice || starGift?.inputInvoice;
if (!inputInvoice) return;
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
if (!requestInputInvoice) {
return;
}
const formId = (starPayment.form?.formId || starPayment.subscriptionInfo?.subscriptionFormId || starGift?.formId)!;
global = updateStarsPayment(global, { status: 'pending' }, tabId);
setGlobal(global);
const result = await callApi('sendStarPaymentForm', {
inputInvoice: requestInputInvoice,
formId,
});
if (!result) {
global = getGlobal();
global = updateStarsPayment(global, { status: 'failed' }, tabId);
setGlobal(global);
actions.closeStarsPaymentModal({ tabId });
actions.closeGiftModal({ tabId });
return;
}
global = getGlobal();
global = updatePayment(global, { status: 'paid' }, tabId);
global = closeInvoice(global, tabId);
global = updateStarsPayment(global, { status: 'paid' }, tabId);
setGlobal(global);
actions.closeStarsPaymentModal({ tabId });
actions.closeGiftModal({ tabId });
if ('channelId' in result) {
actions.openChat({ id: result.channelId, tabId });
}
actions.apiUpdate({
'@type': 'updatePaymentStateCompleted',
inputInvoice,
'@type': 'updateStarPaymentStateCompleted',
paymentState: starGift ? { inputInvoice } : starPayment,
tabId,
});
actions.loadStarStatus();
});
@ -346,7 +406,7 @@ async function sendSmartGlocalCredentials<T extends GlobalState>(
},
};
const tokenizeUrl = selectTabState(global, tabId).payment.nativeParams?.tokenizeUrl;
const tokenizeUrl = selectTabState(global, tabId).payment.form?.nativeParams.tokenizeUrl;
let url;
if (DEBUG_PAYMENT_SMART_GLOCAL) {
@ -474,13 +534,11 @@ addActionHandler('openGiveawayModal', async (global, actions, payload): Promise<
global = getGlobal();
const isOpen = Boolean(chatId);
global = updateTabState(global, {
giveawayModal: {
chatId,
gifts: result,
isOpen,
isOpen: true,
prepaidGiveaway,
starOptions,
},
@ -488,105 +546,24 @@ addActionHandler('openGiveawayModal', async (global, actions, payload): Promise<
setGlobal(global);
});
addActionHandler('closeGiveawayModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giveawayModal: undefined,
}, tabId);
});
addActionHandler('openPremiumGiftingModal', (global, actions, payload): ActionReturnType => {
addActionHandler('openGiftModal', async (global, actions, payload): Promise<void> => {
const {
tabId = getCurrentTabId(),
} = payload || {};
global = getGlobal();
global = updateTabState(global, {
giftingModal: {
isOpen: true,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closePremiumGiftingModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftingModal: undefined,
}, tabId);
});
addActionHandler('openStarsGiftingModal', (global, actions, payload): ActionReturnType => {
const {
tabId = getCurrentTabId(),
} = payload || {};
global = getGlobal();
global = updateTabState(global, {
starsGiftingModal: {
isOpen: true,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closeStarsGiftingModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsGiftingModal: undefined,
}, tabId);
});
addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => {
const {
chatId,
messageId,
tabId = getCurrentTabId(),
} = payload || {};
const message = selectChatMessage(global, chatId, messageId);
if (!message) return undefined;
const transaction = getPrizeStarsTransactionFromGiveaway(message);
if (!transaction) return undefined;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('openPremiumGiftModal', async (global, actions, payload): Promise<void> => {
const {
forUserIds, tabId = getCurrentTabId(),
} = payload || {};
const result = await callApi('fetchPremiumPromo');
if (!result) return;
forUserId, tabId = getCurrentTabId(),
} = payload;
const gifts = await callApi('getPremiumGiftCodeOptions', {});
if (!gifts) return;
global = getGlobal();
global = updateTabState(global, {
giftModal: {
isOpen: true,
forUserIds,
forUserId,
gifts,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closePremiumGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
global = updateTabState(global, {
giftModal: { isOpen: false },
}, tabId);
setGlobal(global);
});
addActionHandler('openStarsGiftModal', async (global, actions, payload): Promise<void> => {
const {
forUserId,
@ -596,7 +573,6 @@ addActionHandler('openStarsGiftModal', async (global, actions, payload): Promise
const starsGiftOptions = await callApi('getStarsGiftOptions', {});
global = getGlobal();
global = updateTabState(global, {
starsGiftModal: {
isOpen: true,
@ -607,14 +583,6 @@ addActionHandler('openStarsGiftModal', async (global, actions, payload): Promise
setGlobal(global);
});
addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
global = updateTabState(global, {
starsGiftModal: { isOpen: false },
}, tabId);
setGlobal(global);
});
addActionHandler('validatePaymentPassword', async (global, actions, payload): Promise<void> => {
const { password, tabId = getCurrentTabId() } = payload;
const result = await callApi('fetchTemporaryPaymentPassword', password);
@ -1022,121 +990,3 @@ addActionHandler('launchPrepaidStarsGiveaway', async (global, actions, payload):
actions.openBoostStatistics({ chatId, tabId });
});
addActionHandler('loadStarStatus', async (global): Promise<void> => {
const currentStatus = global.stars;
const needsTopupOptions = !currentStatus?.topupOptions;
const [status, topupOptions] = await Promise.all([
callApi('fetchStarsStatus'),
needsTopupOptions ? callApi('fetchStarsTopupOptions') : undefined,
]);
if (!status || (needsTopupOptions && !topupOptions)) {
return;
}
global = getGlobal();
global = {
...global,
stars: {
...currentStatus,
balance: status.balance,
topupOptions: topupOptions || currentStatus!.topupOptions,
history: {
all: undefined,
inbound: undefined,
outbound: undefined,
},
subscriptions: undefined,
},
};
if (status.history) {
global = appendStarsTransactions(global, 'all', status.history, status.nextHistoryOffset);
}
if (status.subscriptions) {
global = appendStarsSubscriptions(global, status.subscriptions, status.nextSubscriptionOffset);
}
setGlobal(global);
});
addActionHandler('loadStarsTransactions', async (global, actions, payload): Promise<void> => {
const { type } = payload;
const history = global.stars?.history[type];
const offset = history?.nextOffset;
if (history && !offset) return; // Already loaded all
const result = await callApi('fetchStarsTransactions', {
isInbound: type === 'inbound' || undefined,
isOutbound: type === 'outbound' || undefined,
offset: offset || '',
});
if (!result) {
return;
}
global = getGlobal();
global = updateStarsBalance(global, result.balance);
if (result.history) {
global = appendStarsTransactions(global, type, result.history, result.nextOffset);
}
setGlobal(global);
});
addActionHandler('loadStarsSubscriptions', async (global): Promise<void> => {
const subscriptions = global.stars?.subscriptions;
const offset = subscriptions?.nextOffset;
if (subscriptions && !offset) return; // Already loaded all
const result = await callApi('fetchStarsSubscriptions', {
offset: offset || '',
});
if (!result) {
return;
}
global = getGlobal();
global = updateStarsBalance(global, result.balance);
global = appendStarsSubscriptions(global, result.subscriptions, result.nextOffset);
setGlobal(global);
});
addActionHandler('changeStarsSubscription', async (global, actions, payload): Promise<void> => {
const { peerId, id, isCancelled } = payload;
const peer = peerId ? selectPeer(global, peerId) : undefined;
if (peerId && !peer) return;
await callApi('changeStarsSubscription', {
peer,
subscriptionId: id,
isCancelled,
});
actions.loadStarStatus();
});
addActionHandler('fulfillStarsSubscription', async (global, actions, payload): Promise<void> => {
const { peerId, id } = payload;
const peer = peerId ? selectPeer(global, peerId) : undefined;
if (peerId && !peer) return;
await callApi('fulfillStarsSubscription', {
peer,
subscriptionId: id,
});
actions.loadStarStatus();
});

View File

@ -0,0 +1,264 @@
import type {
StarGiftCategory,
} from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees';
import { callApi } from '../../../api/gramjs';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
appendStarsSubscriptions,
appendStarsTransactions,
updateStarsBalance,
} from '../../reducers';
import {
selectPeer,
selectUser,
} from '../../selectors';
addActionHandler('loadStarStatus', async (global): Promise<void> => {
const currentStatus = global.stars;
const needsTopupOptions = !currentStatus?.topupOptions;
const [status, topupOptions] = await Promise.all([
callApi('fetchStarsStatus'),
needsTopupOptions ? callApi('fetchStarsTopupOptions') : undefined,
]);
if (!status || (needsTopupOptions && !topupOptions)) {
return;
}
global = getGlobal();
global = {
...global,
stars: {
...currentStatus,
balance: status.balance,
topupOptions: topupOptions || currentStatus!.topupOptions,
history: {
all: undefined,
inbound: undefined,
outbound: undefined,
},
subscriptions: undefined,
},
};
if (status.history) {
global = appendStarsTransactions(global, 'all', status.history, status.nextHistoryOffset);
}
if (status.subscriptions) {
global = appendStarsSubscriptions(global, status.subscriptions, status.nextSubscriptionOffset);
}
setGlobal(global);
});
addActionHandler('loadStarsTransactions', async (global, actions, payload): Promise<void> => {
const { type } = payload;
const history = global.stars?.history[type];
const offset = history?.nextOffset;
if (history && !offset) return; // Already loaded all
const result = await callApi('fetchStarsTransactions', {
isInbound: type === 'inbound' || undefined,
isOutbound: type === 'outbound' || undefined,
offset: offset || '',
});
if (!result) {
return;
}
global = getGlobal();
global = updateStarsBalance(global, result.balance);
if (result.history) {
global = appendStarsTransactions(global, type, result.history, result.nextOffset);
}
setGlobal(global);
});
addActionHandler('loadStarGifts', async (global): Promise<void> => {
const result = await callApi('fetchStarGifts');
if (!result) {
return;
}
const { gifts, stickers } = result;
const starGiftsById = buildCollectionByKey(gifts, 'id');
const starGiftCategoriesByName: Record<StarGiftCategory, string[]> = {
all: [],
limited: [],
};
const allStarGiftIds = Object.keys(starGiftsById);
const allStarGifts = Object.values(starGiftsById);
const limitedStarGiftIds = allStarGifts.map(
(gift) => {
return gift.isLimited ? gift.id : undefined;
},
).filter(Boolean) as string[];
starGiftCategoriesByName.all = allStarGiftIds;
starGiftCategoriesByName.limited = limitedStarGiftIds;
allStarGifts.forEach((gift) => {
const starsCategory = gift.stars;
if (!starGiftCategoriesByName[starsCategory]) {
starGiftCategoriesByName[starsCategory] = [];
}
starGiftCategoriesByName[starsCategory].push(gift.id);
});
global = getGlobal();
global = {
...global,
starGiftsById,
starGiftCategoriesByName,
stickers: {
...global.stickers,
starGifts: {
stickers,
},
},
};
setGlobal(global);
});
addActionHandler('loadUserGifts', async (global, actions, payload): Promise<void> => {
const { userId, shouldRefresh } = payload;
const user = selectUser(global, userId);
if (!user) return;
const currentGifts = global.users.giftsById[userId];
const localNextOffset = currentGifts?.nextOffset;
if (!shouldRefresh && currentGifts && !localNextOffset) return; // Already loaded all
const result = await callApi('fetchUserStarGifts', {
user,
offset: !shouldRefresh ? localNextOffset : '',
});
if (!result) {
return;
}
global = getGlobal();
const newGifts = currentGifts && !shouldRefresh ? currentGifts.gifts.concat(result.gifts) : result.gifts;
global = {
...global,
users: {
...global.users,
giftsById: {
...global.users.giftsById,
[userId]: {
gifts: newGifts,
nextOffset: result.nextOffset,
},
},
},
};
setGlobal(global);
});
addActionHandler('loadStarsSubscriptions', async (global): Promise<void> => {
const subscriptions = global.stars?.subscriptions;
const offset = subscriptions?.nextOffset;
if (subscriptions && !offset) return; // Already loaded all
const result = await callApi('fetchStarsSubscriptions', {
offset: offset || '',
});
if (!result) {
return;
}
global = getGlobal();
global = updateStarsBalance(global, result.balance);
global = appendStarsSubscriptions(global, result.subscriptions, result.nextOffset);
setGlobal(global);
});
addActionHandler('changeStarsSubscription', async (global, actions, payload): Promise<void> => {
const { peerId, id, isCancelled } = payload;
const peer = peerId ? selectPeer(global, peerId) : undefined;
if (peerId && !peer) return;
await callApi('changeStarsSubscription', {
peer,
subscriptionId: id,
isCancelled,
});
actions.loadStarStatus();
});
addActionHandler('fulfillStarsSubscription', async (global, actions, payload): Promise<void> => {
const { peerId, id } = payload;
const peer = peerId ? selectPeer(global, peerId) : undefined;
if (peerId && !peer) return;
await callApi('fulfillStarsSubscription', {
peer,
subscriptionId: id,
});
actions.loadStarStatus();
});
addActionHandler('changeGiftVisilibity', async (global, actions, payload): Promise<void> => {
const { userId, messageId, shouldUnsave } = payload;
const user = selectUser(global, userId);
if (!user) return;
const result = await callApi('saveStarGift', {
user,
messageId,
shouldUnsave,
});
if (!result) {
return;
}
actions.loadUserGifts({ userId: global.currentUserId!, shouldRefresh: true });
});
addActionHandler('convertGiftToStars', async (global, actions, payload): Promise<void> => {
const { userId, messageId, tabId = getCurrentTabId() } = payload;
const user = selectUser(global, userId);
if (!user) return;
const result = await callApi('convertStarGift', {
user,
messageId,
});
if (!result) {
return;
}
actions.loadUserGifts({ userId: global.currentUserId!, shouldRefresh: true });
actions.openStarsBalanceModal({ tabId });
});

View File

@ -73,7 +73,7 @@ addActionHandler('sync', (global, actions): ActionReturnType => {
loadAllChats({
listType: 'active',
onFirstBatchDone: async () => {
whenFirstBatchDone: async () => {
await loadAndReplaceMessages(global, actions);
global = getGlobal();

View File

@ -1,104 +1,101 @@
import type { ActionReturnType } from '../../types';
import { STARS_CURRENCY_CODE } from '../../../config';
import { areDeepEqual } from '../../../util/areDeepEqual';
import { formatCurrencyAsString } from '../../../util/formatCurrency';
import * as langProvider from '../../../util/oldLangProvider';
import { addActionHandler, setGlobal } from '../../index';
import { closeInvoice, updateStarsBalance } from '../../reducers';
import { updateStarsBalance } from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { selectTabState } from '../../selectors';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
case 'updatePaymentStateCompleted': {
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const { inputInvoice, invoice } = selectTabState(global, tabId).payment;
const { paymentState, tabId } = update;
const form = paymentState.form!;
const { invoice } = form;
if (!areDeepEqual(inputInvoice, update.inputInvoice)) return;
const { totalAmount, currency } = invoice;
if (invoice) {
const { amount, currency, title } = invoice;
if (currency !== STARS_CURRENCY_CODE) {
actions.showNotification({
tabId,
message: langProvider.oldTranslate('PaymentInfoHint', [
formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code),
title,
]),
});
}
}
if (inputInvoice?.type === 'giftcode') {
if (!inputInvoice.userIds) {
return;
}
const giftModalState = selectTabState(global, tabId).giftModal;
if (giftModalState && giftModalState.isOpen
&& areDeepEqual(inputInvoice.userIds, giftModalState.forUserIds)) {
global = updateTabState(global, {
giftModal: {
...giftModalState,
isCompleted: true,
},
}, tabId);
}
}
if (inputInvoice?.type === 'starsgift') {
if (!inputInvoice.userId) {
return;
}
const starsModalState = selectTabState(global, tabId).starsGiftModal;
if (starsModalState && starsModalState.isOpen
&& areDeepEqual(inputInvoice.userId, starsModalState.forUserId)) {
global = updateTabState(global, {
starsGiftModal: {
...starsModalState,
isCompleted: true,
},
}, tabId);
}
}
if (inputInvoice?.type === 'stars') {
const starsModalState = selectTabState(global, tabId).starsGiftModal;
if (starsModalState && starsModalState.isOpen) {
global = updateTabState(global, {
starsGiftModal: {
...starsModalState,
isCompleted: true,
},
}, tabId);
}
actions.loadStarStatus(); // Manually reload. Server update takes ~10 seconds
}
if (inputInvoice?.type === 'chatInviteSubscription') {
const { amount } = invoice!;
actions.showNotification({
tabId,
title: langProvider.oldTranslate('StarsSubscriptionCompleted'),
message: langProvider.oldTranslate('StarsSubscriptionCompletedText', [
amount,
inputInvoice.inviteInfo.title,
], undefined, amount),
icon: 'star',
});
}
if (invoice?.currency === STARS_CURRENCY_CODE) {
global = closeInvoice(global, tabId);
}
setGlobal(global);
actions.showNotification({
tabId,
message: langProvider.oldTranslate('PaymentInfoHint', [
formatCurrencyAsString(totalAmount, currency, langProvider.getTranslationFn().code),
form.title,
]),
});
setGlobal(global);
break;
}
case 'updateStarPaymentStateCompleted': {
const { paymentState, tabId } = update;
const { inputInvoice, subscriptionInfo } = paymentState;
if (inputInvoice?.type === 'chatInviteSubscription' && subscriptionInfo) {
const amount = subscriptionInfo.subscriptionPricing!.amount;
actions.showNotification({
tabId,
title: langProvider.oldTranslate('StarsSubscriptionCompleted'),
message: langProvider.oldTranslate('StarsSubscriptionCompletedText', [
amount,
subscriptionInfo.title,
], undefined, amount),
icon: 'star',
});
}
if (inputInvoice?.type === 'giftcode') {
if (!inputInvoice.userIds) {
return;
}
const giftModalState = selectTabState(global, tabId).giftModal;
if (giftModalState && inputInvoice.userIds[0] === giftModalState.forUserId) {
global = updateTabState(global, {
giftModal: {
...giftModalState,
isCompleted: true,
},
}, tabId);
}
}
if (inputInvoice?.type === 'starsgift') {
if (!inputInvoice.userId) {
return;
}
const starsModalState = selectTabState(global, tabId).starsGiftModal;
if (starsModalState && starsModalState.isOpen
&& areDeepEqual(inputInvoice.userId, starsModalState.forUserId)) {
global = updateTabState(global, {
starsGiftModal: {
...starsModalState,
isCompleted: true,
},
}, tabId);
}
}
if (inputInvoice?.type === 'stars') {
const starsModalState = selectTabState(global, tabId).starsGiftModal;
if (starsModalState && starsModalState.isOpen) {
global = updateTabState(global, {
starsGiftModal: {
...starsModalState,
isCompleted: true,
},
}, tabId);
}
}
if (inputInvoice?.type === 'stars' || inputInvoice?.type === 'stargift') {
actions.requestConfetti({ withStars: true, tabId });
}
break;
}

View File

@ -1,51 +1,40 @@
import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler } from '../../index';
import {
clearPayment, closeInvoice, openStarsTransactionModal, updatePayment,
clearPayment,
updatePayment,
updateStarsPayment,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { selectChatMessage, selectTabState } from '../../selectors';
import { selectTabState } from '../../selectors';
addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const payment = selectTabState(global, tabId).payment;
const status = payment.status || 'cancelled';
const starsBalanceModal = selectTabState(global, tabId).starsBalanceModal;
const originPayment = starsBalanceModal?.originPayment;
const originReaction = starsBalanceModal?.originReaction;
actions.processOriginStarsPayment({
originData: starsBalanceModal,
status,
tabId,
});
global = clearPayment(global, tabId);
global = closeInvoice(global, tabId);
global = updateTabState(global, {
payment: {
...selectTabState(global, tabId).payment,
status,
},
...((originPayment || originReaction) && {
starsBalanceModal: undefined,
}),
global = updatePayment(global, {
status,
}, tabId);
// Re-open previous payment modal
if (originPayment) {
global = updatePayment(global, originPayment, tabId);
global = updateTabState(global, {
isStarPaymentModalOpen: true,
}, tabId);
}
return global;
});
// Send reaction
if (originReaction) {
actions.sendPaidReaction({
chatId: originReaction.chatId,
messageId: originReaction.messageId,
forcedAmount: originReaction.amount,
tabId,
});
}
addActionHandler('resetPaymentStatus', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
global = updatePayment(global, { status: undefined }, tabId);
global = updateStarsPayment(global, { status: undefined }, tabId);
return global;
});
@ -61,6 +50,14 @@ addActionHandler('addPaymentError', (global, actions, payload): ActionReturnType
}, tabId);
});
addActionHandler('closeGiveawayModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giveawayModal: undefined,
}, tabId);
});
addActionHandler('closeGiftCodeModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
@ -68,74 +65,3 @@ addActionHandler('closeGiftCodeModal', (global, actions, payload): ActionReturnT
giftCodeModal: undefined,
}, tabId);
});
addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionReturnType => {
const { originPayment, originReaction, tabId = getCurrentTabId() } = payload || {};
global = clearPayment(global, tabId);
// Always refresh status on opening
actions.loadStarStatus();
return updateTabState(global, {
starsBalanceModal: {
originPayment,
originReaction,
},
}, tabId);
});
addActionHandler('closeStarsBalanceModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsBalanceModal: undefined,
}, tabId);
});
addActionHandler('openStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { transaction, tabId = getCurrentTabId() } = payload;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('openStarsTransactionFromGift', (global, actions, payload): ActionReturnType => {
const {
chatId,
messageId,
tabId = getCurrentTabId(),
} = payload || {};
const message = selectChatMessage(global, chatId, messageId);
if (!message) return undefined;
const transaction = getStarsTransactionFromGift(message);
if (!transaction) return undefined;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsTransactionModal: undefined,
}, tabId);
});
addActionHandler('openStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { subscription, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
starsSubscriptionModal: {
subscription,
},
}, tabId);
});
addActionHandler('closeStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsSubscriptionModal: undefined,
}, tabId);
});

View File

@ -0,0 +1,258 @@
import type { ApiUserStarGift } from '../../../api/types';
import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getPrizeStarsTransactionFromGiveaway, getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler } from '../../index';
import {
clearStarPayment, openStarsTransactionModal,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { selectChatMessage, selectStarsPayment } from '../../selectors';
addActionHandler('processOriginStarsPayment', (global, actions, payload): ActionReturnType => {
const { originData, status, tabId = getCurrentTabId() } = payload;
const { originStarsPayment, originReaction, originGift } = originData || {};
actions.closeStarsBalanceModal({ tabId });
if (status !== 'paid') {
return undefined;
}
// Re-open previous payment modal
if (originStarsPayment) {
global = updateTabState(global, {
starsPayment: originStarsPayment,
}, tabId);
}
if (originReaction) {
actions.sendPaidReaction({
chatId: originReaction.chatId,
messageId: originReaction.messageId,
forcedAmount: originReaction.amount,
tabId,
});
}
if (originGift) {
actions.sendStarGift({
...originGift,
tabId,
});
}
return global;
});
addActionHandler('openGiftRecipientPicker', (global, actions, payload): ActionReturnType => {
const {
tabId = getCurrentTabId(),
} = payload || {};
return updateTabState(global, {
isGiftRecipientPickerOpen: true,
}, tabId);
});
addActionHandler('closeGiftRecipientPicker', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
isGiftRecipientPickerOpen: undefined,
}, tabId);
});
addActionHandler('openStarsGiftingPickerModal', (global, actions, payload): ActionReturnType => {
const {
tabId = getCurrentTabId(),
} = payload || {};
return updateTabState(global, {
starsGiftingPickerModal: {
isOpen: true,
},
}, tabId);
});
addActionHandler('closeStarsGiftingPickerModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsGiftingPickerModal: undefined,
}, tabId);
});
addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => {
const {
chatId,
messageId,
tabId = getCurrentTabId(),
} = payload || {};
const message = selectChatMessage(global, chatId, messageId);
if (!message) return undefined;
const transaction = getPrizeStarsTransactionFromGiveaway(message);
if (!transaction) return undefined;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionReturnType => {
const {
originStarsPayment,
originReaction,
originGift,
tabId = getCurrentTabId(),
} = payload || {};
global = clearStarPayment(global, tabId);
// Always refresh status on opening
actions.loadStarStatus();
return updateTabState(global, {
starsBalanceModal: {
originStarsPayment,
originReaction,
originGift,
},
}, tabId);
});
addActionHandler('closeStarsBalanceModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsBalanceModal: undefined,
}, tabId);
});
addActionHandler('closeStarsPaymentModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const starsPayment = selectStarsPayment(global, tabId);
let status = starsPayment?.status;
if (!status || status === 'pending') {
status = 'cancelled';
}
return updateTabState(global, {
starsPayment: {
status,
},
}, tabId);
});
addActionHandler('openStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { transaction, tabId = getCurrentTabId() } = payload;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('openStarsTransactionFromGift', (global, actions, payload): ActionReturnType => {
const {
chatId,
messageId,
tabId = getCurrentTabId(),
} = payload || {};
const message = selectChatMessage(global, chatId, messageId);
if (!message) return undefined;
const transaction = getStarsTransactionFromGift(message);
if (!transaction) return undefined;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsTransactionModal: undefined,
}, tabId);
});
addActionHandler('openStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { subscription, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
starsSubscriptionModal: {
subscription,
},
}, tabId);
});
addActionHandler('closeStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsSubscriptionModal: undefined,
}, tabId);
});
addActionHandler('closeGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftModal: undefined,
}, tabId);
});
addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsGiftModal: { isOpen: false },
}, tabId);
});
addActionHandler('openGiftInfoModalFromMessage', (global, actions, payload): ActionReturnType => {
const {
chatId, messageId, tabId = getCurrentTabId(),
} = payload;
const message = selectChatMessage(global, chatId, messageId);
if (!message || !message.content.action) return;
const action = message.content.action;
if (action.type !== 'starGift') return;
const starGift = action.starGift!;
const giftReceiverId = message.isOutgoing ? message.chatId : global.currentUserId!;
const gift = {
date: message.date,
gift: starGift.gift,
message: starGift.message,
starsToConvert: starGift.starsToConvert,
isNameHidden: starGift.isNameHidden,
isUnsaved: !starGift.isSaved,
fromId: message.isOutgoing ? global.currentUserId : message.chatId,
messageId: (!message.isOutgoing || chatId === global.currentUserId) ? message.id : undefined,
isConverted: starGift.isConverted,
} satisfies ApiUserStarGift;
actions.openGiftInfoModal({ userId: giftReceiverId, gift, tabId });
});
addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnType => {
const {
userId, gift, tabId = getCurrentTabId(),
} = payload;
return updateTabState(global, {
giftInfoModal: {
userId,
gift,
},
}, tabId);
});
addActionHandler('closeGiftInfoModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftInfoModal: undefined,
}, tabId);
});

View File

@ -256,6 +256,11 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.chats.topicsInfoById) {
cached.chats.topicsInfoById = initialState.chats.topicsInfoById;
}
if (!cached.stickers.starGifts) {
cached.stickers.starGifts = initialState.stickers.starGifts;
cached.users.giftsById = initialState.users.giftsById;
}
}
function updateCache(force?: boolean) {

View File

@ -199,7 +199,7 @@ function getSummaryDescription(
}
if (invoice) {
summary = invoice.extendedMedia ? invoice.title : `${lang('PaymentInvoice')}: ${invoice.text}`;
summary = invoice.extendedMedia ? invoice.title : `${lang('PaymentInvoice')}: ${invoice.description}`;
}
if (text) {

Some files were not shown because too many files have changed in this diff Show More