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:
parent
23375d3175
commit
1dc29627bd
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -235,7 +235,8 @@ export interface ApiAppConfig {
|
||||
channelRestrictAdsLevelMin?: number;
|
||||
paidReactionMaxAmount?: number;
|
||||
isChannelRevenueWithdrawalEnabled?: boolean;
|
||||
isStarsGiftsEnabled?: boolean;
|
||||
isStarsGiftEnabled?: boolean;
|
||||
starGiftMaxMessageLength?: number;
|
||||
}
|
||||
|
||||
export interface ApiConfig {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 can’t 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 a–z, 0–9 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 can’t 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 can’t 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";
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
16
src/components/common/BadgeButton.module.scss
Normal file
16
src/components/common/BadgeButton.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/components/common/BadgeButton.tsx
Normal file
25
src/components/common/BadgeButton.tsx
Normal 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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
18
src/components/common/gift/GiftRibbon.module.scss
Normal file
18
src/components/common/gift/GiftRibbon.module.scss
Normal 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;
|
||||
}
|
||||
48
src/components/common/gift/GiftRibbon.tsx
Normal file
48
src/components/common/gift/GiftRibbon.tsx
Normal 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);
|
||||
62
src/components/common/gift/UserGift.module.scss
Normal file
62
src/components/common/gift/UserGift.module.scss
Normal 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);
|
||||
}
|
||||
89
src/components/common/gift/UserGift.tsx
Normal file
89
src/components/common/gift/UserGift.tsx
Normal 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));
|
||||
@ -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',
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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))}
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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));
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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) && (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
142
src/components/modals/gift/GiftComposer.module.scss
Normal file
142
src/components/modals/gift/GiftComposer.module.scss
Normal 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;
|
||||
}
|
||||
259
src/components/modals/gift/GiftComposer.tsx
Normal file
259
src/components/modals/gift/GiftComposer.tsx
Normal 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));
|
||||
65
src/components/modals/gift/GiftItem.module.scss
Normal file
65
src/components/modals/gift/GiftItem.module.scss
Normal 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
|
||||
}
|
||||
102
src/components/modals/gift/GiftItemPremium.tsx
Normal file
102
src/components/modals/gift/GiftItemPremium.tsx
Normal 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));
|
||||
89
src/components/modals/gift/GiftItemStar.tsx
Normal file
89
src/components/modals/gift/GiftItemStar.tsx
Normal 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));
|
||||
18
src/components/modals/gift/GiftModal.async.tsx
Normal file
18
src/components/modals/gift/GiftModal.async.tsx
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
334
src/components/modals/gift/GiftModal.tsx
Normal file
334
src/components/modals/gift/GiftModal.tsx
Normal 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;
|
||||
}
|
||||
50
src/components/modals/gift/StarGiftCategoryList.module.scss
Normal file
50
src/components/modals/gift/StarGiftCategoryList.module.scss
Normal 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;
|
||||
}
|
||||
98
src/components/modals/gift/StarGiftCategoryList.tsx
Normal file
98
src/components/modals/gift/StarGiftCategoryList.tsx
Normal 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));
|
||||
18
src/components/modals/gift/info/GiftInfoModal.async.tsx
Normal file
18
src/components/modals/gift/info/GiftInfoModal.async.tsx
Normal 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;
|
||||
42
src/components/modals/gift/info/GiftInfoModal.module.scss
Normal file
42
src/components/modals/gift/info/GiftInfoModal.module.scss
Normal 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;
|
||||
}
|
||||
274
src/components/modals/gift/info/GiftInfoModal.tsx
Normal file
274
src/components/modals/gift/info/GiftInfoModal.tsx
Normal 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));
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
95
src/components/modals/gift/recipient/GiftRecipientPicker.tsx
Normal file
95
src/components/modals/gift/recipient/GiftRecipientPicker.tsx
Normal 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));
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
23
src/components/modals/stars/helpers/transaction.ts
Normal file
23
src/components/modals/stars/helpers/transaction.ts
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
264
src/global/actions/api/stars.ts
Normal file
264
src/global/actions/api/stars.ts
Normal 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 });
|
||||
});
|
||||
@ -73,7 +73,7 @@ addActionHandler('sync', (global, actions): ActionReturnType => {
|
||||
|
||||
loadAllChats({
|
||||
listType: 'active',
|
||||
onFirstBatchDone: async () => {
|
||||
whenFirstBatchDone: async () => {
|
||||
await loadAndReplaceMessages(global, actions);
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
258
src/global/actions/ui/stars.ts
Normal file
258
src/global/actions/ui/stars.ts
Normal 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);
|
||||
});
|
||||
@ -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) {
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user