From 6f292c9032f31f503f6dd0d6c848f9309b4d9732 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 29 Aug 2024 15:52:21 +0200 Subject: [PATCH] Stars Gifting: Implement Stars Gifting for users (#4847) --- src/api/gramjs/apiBuilders/messages.ts | 27 ++- src/api/gramjs/apiBuilders/payments.ts | 15 +- src/api/gramjs/gramjsBuilders/index.ts | 9 + src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/methods/payments.ts | 18 +- src/api/types/messages.ts | 12 +- src/api/types/payments.ts | 17 +- src/assets/localization/fallback.strings | 3 + src/bundles/extra.ts | 7 +- src/components/common/Picker.tsx | 0 .../helpers/renderActionMessageText.tsx | 47 +++-- src/components/common/pickers/PickerModal.tsx | 3 + .../common/profile/UserBirthday.tsx | 10 +- src/components/left/settings/SettingsMain.tsx | 40 ++-- src/components/main/Main.tsx | 22 +- .../main/premium/GiftPremiumModal.module.scss | 46 ---- ...l.async.tsx => PremiumGiftModal.async.tsx} | 10 +- .../main/premium/PremiumGiftModal.module.scss | 123 +++++++++++ ...tPremiumModal.tsx => PremiumGiftModal.tsx} | 53 ++--- .../premium/PremiumGiftingModal.async.tsx | 18 -- .../PremiumGiftingPickerModal.async.tsx | 18 ++ ... => PremiumGiftingPickerModal.module.scss} | 0 ...odal.tsx => PremiumGiftingPickerModal.tsx} | 89 +++----- .../main/premium/StarsGiftModal.async.tsx | 18 ++ .../main/premium/StarsGiftModal.module.scss | 107 ++++++++++ .../main/premium/StarsGiftModal.tsx | 197 ++++++++++++++++++ .../premium/StarsGiftingPickerModal.async.tsx | 18 ++ .../StarsGiftingPickerModal.module.scss | 86 ++++++++ .../main/premium/StarsGiftingPickerModal.tsx | 141 +++++++++++++ src/components/middle/ActionMessage.tsx | 75 +++++-- src/components/middle/HeaderMenuContainer.tsx | 4 +- src/components/middle/MessageList.scss | 5 + src/components/middle/MiddleColumn.tsx | 17 +- .../modals/common/TableInfoModal.module.scss | 24 ++- .../modals/common/TableInfoModal.tsx | 12 +- .../modals/stars/StarGiftInfoModal.async.tsx | 18 ++ .../stars/StarGiftInfoModal.module.scss | 32 +++ .../modals/stars/StarGiftInfoModal.tsx | 150 +++++++++++++ .../stars/StarTopupOptionList.module.scss | 62 ++++++ .../modals/stars/StarTopupOptionList.tsx | 110 ++++++++++ .../stars/StarsBalanceModal.module.scss | 66 ++---- .../modals/stars/StarsBalanceModal.tsx | 135 +++++------- src/config.ts | 2 +- src/global/actions/api/payments.ts | 89 +++++++- src/global/actions/apiUpdaters/payments.ts | 22 +- src/global/helpers/payments.ts | 20 ++ src/global/types.ts | 44 +++- src/types/language.d.ts | 11 +- src/util/localization/index.ts | 1 + 49 files changed, 1668 insertions(+), 387 deletions(-) create mode 100644 src/components/common/Picker.tsx delete mode 100644 src/components/main/premium/GiftPremiumModal.module.scss rename src/components/main/premium/{GiftPremiumModal.async.tsx => PremiumGiftModal.async.tsx} (51%) create mode 100644 src/components/main/premium/PremiumGiftModal.module.scss rename src/components/main/premium/{GiftPremiumModal.tsx => PremiumGiftModal.tsx} (81%) delete mode 100644 src/components/main/premium/PremiumGiftingModal.async.tsx create mode 100644 src/components/main/premium/PremiumGiftingPickerModal.async.tsx rename src/components/main/premium/{PremiumGiftingModal.module.scss => PremiumGiftingPickerModal.module.scss} (100%) rename src/components/main/premium/{PremiumGiftingModal.tsx => PremiumGiftingPickerModal.tsx} (53%) create mode 100644 src/components/main/premium/StarsGiftModal.async.tsx create mode 100644 src/components/main/premium/StarsGiftModal.module.scss create mode 100644 src/components/main/premium/StarsGiftModal.tsx create mode 100644 src/components/main/premium/StarsGiftingPickerModal.async.tsx create mode 100644 src/components/main/premium/StarsGiftingPickerModal.module.scss create mode 100644 src/components/main/premium/StarsGiftingPickerModal.tsx create mode 100644 src/components/modals/stars/StarGiftInfoModal.async.tsx create mode 100644 src/components/modals/stars/StarGiftInfoModal.module.scss create mode 100644 src/components/modals/stars/StarGiftInfoModal.tsx create mode 100644 src/components/modals/stars/StarTopupOptionList.module.scss create mode 100644 src/components/modals/stars/StarTopupOptionList.tsx diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index ec5ef73aa..2192554c1 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -352,6 +352,7 @@ function buildAction( let phoneCall: PhoneCallAction | undefined; let call: Partial | undefined; let amount: number | undefined; + let stars: number | undefined; let currency: string | undefined; let giftCryptoInfo: { currency: string; @@ -370,11 +371,11 @@ function buildAction( let isUnclaimed: boolean | undefined; let pluralValue: number | undefined; - const targetUserIds = 'users' in action + let targetUserIds = 'users' in action ? action.users && action.users.map((id) => buildApiPeerId(id, 'user')) : ('userId' in action && [buildApiPeerId(action.userId, 'user')]) || []; - let targetChatId: string | undefined; + let targetChatId; if (action instanceof GramJs.MessageActionChatCreate) { text = 'Notification.CreatedChatWithTitle'; translationValues.push('%action_origin%', action.title); @@ -611,6 +612,27 @@ function buildAction( text = 'ActionRefunded'; amount = Number(action.totalAmount); currency = action.currency; + } else if (action instanceof GramJs.MessageActionRequestedPeer) { + text = 'ActionRequestedPeer'; + if (action.peers) { + targetUserIds = action.peers?.map((peer) => getApiChatIdFromMtpPeer(peer)); + } + if (targetPeerId) { + translationValues.unshift('%action_origin%'); + } + } else if (action instanceof GramJs.MessageActionGiftStars) { + text = isOutgoing ? 'ActionGiftOutbound' : 'BoostingReceivedGiftNoName'; + if (isOutgoing) { + translationValues.push('%gift_payment_amount%'); + } else { + translationValues.push('%action_origin%', '%gift_payment_amount%'); + } + if (targetPeerId) { + targetUserIds.push(targetPeerId); + } + currency = action.currency; + amount = action.amount.toJSNumber(); + stars = action.stars.toJSNumber(); } else { text = 'ChatList.UnsupportedMessage'; } @@ -628,6 +650,7 @@ function buildAction( targetChatId, photo, // TODO Only used internally now, will be used for the UI in future amount, + stars, currency, giftCryptoInfo, isGiveaway, diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 903bfc4b7..72a82fcab 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -8,7 +8,7 @@ import type { ApiGiveawayInfo, ApiInvoice, ApiLabeledPrice, ApiMyBoost, ApiPaymentCredentials, ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumGiftCodeOption, ApiPremiumPromo, ApiPremiumSubscriptionOption, - ApiReceipt, + ApiReceipt, ApiStarsGiftOption, ApiStarsTransaction, ApiStarsTransactionPeer, ApiStarTopupOption, @@ -383,6 +383,19 @@ export function buildApiPremiumGiftCodeOption(option: GramJs.PremiumGiftCodeOpti }; } +export function buildApiStarsGiftOptions(option: GramJs.StarsGiftOption): ApiStarsGiftOption { + const { + extended, stars, amount, currency, + } = option; + + return { + isExtended: extended, + stars: stars.toJSNumber(), + amount: amount.toJSNumber(), + currency, + }; +} + export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPeer): ApiStarsTransactionPeer { if (peer instanceof GramJs.StarsTransactionPeerAppStore) { return { type: 'appStore' }; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 5128dd8e7..70c298cd4 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -546,6 +546,15 @@ GramJs.TypeInputStorePaymentPurpose { }); } + if (purpose.type === 'starsgift') { + return new GramJs.InputStorePaymentStarsGift({ + userId: buildInputEntity(purpose.user.id, purpose.user.accessHash) as GramJs.InputUser, + stars: BigInt(purpose.stars), + currency: purpose.currency, + amount: BigInt(purpose.amount), + }); + } + if (purpose.type === 'giftcode') { return new GramJs.InputStorePaymentPremiumGiftCode({ users: purpose.users.map((user) => buildInputEntity(user.id, user.accessHash) as GramJs.InputUser), diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index c87bb4413..93ec6dd24 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -101,7 +101,7 @@ export { validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword, applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode, getPremiumGiftCodeOptions, launchPrepaidGiveaway, fetchStarsStatus, fetchStarsTopupOptions, fetchStarsTransactions, - sendStarPaymentForm, + sendStarPaymentForm, getStarsGiftOptions, } from './payments'; export * from './fragment'; diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index ee104ef4f..7ee774505 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -19,7 +19,7 @@ import { buildApiPaymentForm, buildApiPremiumGiftCodeOption, buildApiPremiumPromo, - buildApiReceipt, + buildApiReceipt, buildApiStarsGiftOptions, buildApiStarsTransaction, buildApiStarTopupOption, buildShippingOptions, @@ -403,6 +403,22 @@ export async function getPremiumGiftCodeOptions({ return result.map(buildApiPremiumGiftCodeOption); } +export async function getStarsGiftOptions({ + chat, +}: { + chat?: ApiChat; +}) { + const result = await invokeRequest(new GramJs.payments.GetStarsGiftOptions({ + userId: chat && buildInputPeer(chat.id, chat.accessHash), + })); + + if (!result) { + return undefined; + } + + return result.map(buildApiStarsGiftOptions); +} + export function launchPrepaidGiveaway({ chat, giveawayId, diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index cf5db425c..a92407fe1 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -240,8 +240,16 @@ export type ApiInputInvoiceStars = { amount: number; }; +export type ApiInputInvoiceStarsGift = { + type: 'starsgift'; + userId: string; + stars: number; + currency: string; + amount: number; +}; + export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway -| ApiInputInvoiceGiftCode | ApiInputInvoiceStars; +| ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars; /* Used for Invoice request */ export type ApiRequestInputInvoiceMessage = { @@ -392,6 +400,8 @@ export interface ApiAction { | 'other'; photo?: ApiPhoto; amount?: number; + stars?: number; + transactionId?: string; currency?: string; giftCryptoInfo?: { currency: string; diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index fe4f98a05..80c95a9e9 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -142,8 +142,16 @@ export type ApiInputStorePaymentStarsTopup = { amount: number; }; +export type ApiInputStorePaymentStarsGift = { + type: 'starsgift'; + user: ApiUser; + stars: number; + currency: string; + amount: number; +}; + export type ApiInputStorePaymentPurpose = ApiInputStorePaymentGiveaway | ApiInputStorePaymentGiftcode | -ApiInputStorePaymentStarsTopup; +ApiInputStorePaymentStarsTopup | ApiInputStorePaymentStarsGift; export interface ApiPremiumGiftCodeOption { users: number; @@ -152,6 +160,13 @@ export interface ApiPremiumGiftCodeOption { amount: number; } +export interface ApiStarsGiftOption { + isExtended?: true; + stars: number; + currency: string; + amount: number; +} + export type ApiBoostsStatus = { level: number; currentLevelBoosts: number; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 990dbf4ea..3350aaac8 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1273,3 +1273,6 @@ "ReplyInPrivateMessage" = "Reply In Private Message"; "AriaSearchOlderResult" = "Focus next result"; "AriaSearchNewerResult" = "Focus previous result"; +"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}" +"CreditsBoxOutAbout" = "Review the {link} for Stars." +"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram." diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index a48735e31..1695dd434 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -16,13 +16,16 @@ 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 GiftPremiumModal } from '../components/main/premium/GiftPremiumModal'; +export { default as PremiumGiftModal } from '../components/main/premium/PremiumGiftModal'; +export { default as StarsGiftModal } from '../components/main/premium/StarsGiftModal'; export { default as GiveawayModal } from '../components/main/premium/GiveawayModal'; -export { default as PremiumGiftingModal } from '../components/main/premium/PremiumGiftingModal'; +export { default as PremiumGiftingPickerModal } from '../components/main/premium/PremiumGiftingPickerModal'; +export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal'; 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'; export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal'; +export { default as StarGiftInfoModal } from '../components/modals/stars/StarGiftInfoModal'; export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal'; export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal'; export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal'; diff --git a/src/components/common/Picker.tsx b/src/components/common/Picker.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index 3cc088db0..bde0cf1b4 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -84,8 +84,41 @@ export function renderActionMessageText( .replace('un1', '%action_origin%') .replace('%1$s', '%gift_payment_amount%'); } + if (translationKey === 'ActionRequestedPeer') { + unprocessed = unprocessed + .replace('un1', '%star_target_user%') + .replace('un2', '%action_origin%') + .replace(/\*\*/g, ''); + } let processed: TextPart[]; + if (unprocessed.includes('%star_target_user%')) { + processed = processPlaceholder( + unprocessed, + '%star_target_user%', + targetUsers + ? targetUsers.map((user) => renderUserContent(user, noLinks)).filter(Boolean) + : 'User', + ); + + unprocessed = processed.pop() as string; + content.push(...processed); + } + + processed = processPlaceholder( + unprocessed, + '%action_origin%', + actionOriginChat ? ( + renderChatContent(lang, actionOriginChat, noLinks) || NBSP + ) : actionOriginUser ? ( + renderUserContent(actionOriginUser, noLinks) || NBSP + ) : 'User', + '', + ); + + unprocessed = processed.pop() as string; + content.push(...processed); + if (unprocessed.includes('%payment_amount%')) { processed = processPlaceholder( unprocessed, @@ -96,20 +129,6 @@ export function renderActionMessageText( content.push(...processed); } - processed = processPlaceholder( - unprocessed, - '%action_origin%', - actionOriginUser ? ( - renderUserContent(actionOriginUser, noLinks) || NBSP - ) : actionOriginChat ? ( - renderChatContent(lang, actionOriginChat, noLinks) || NBSP - ) : 'User', - '', - ); - - unprocessed = processed.pop() as string; - content.push(...processed); - if (unprocessed.includes('%action_topic%')) { const topicEmoji = topic?.iconEmojiId ? diff --git a/src/components/common/pickers/PickerModal.tsx b/src/components/common/pickers/PickerModal.tsx index 61ad6de0b..a02f06423 100644 --- a/src/components/common/pickers/PickerModal.tsx +++ b/src/components/common/pickers/PickerModal.tsx @@ -14,6 +14,7 @@ type OwnProps = { isConfirmDisabled?: boolean; shouldAdaptToSearch?: boolean; withFixedHeight?: boolean; + withPremiumGradient?: boolean; onConfirm?: NoneToVoidFunction; } & ModalProps; @@ -23,6 +24,7 @@ const PickerModal = ({ shouldAdaptToSearch, withFixedHeight, onConfirm, + withPremiumGradient, ...modalProps }: OwnProps) => { const lang = useOldLang(); @@ -43,6 +45,7 @@ const PickerModal = ({ {modalProps.children}
@@ -176,7 +176,7 @@ const SettingsMain: FC = ({ // eslint-disable-next-line react/jsx-no-bind onClick={() => openPremiumModal()} > - {lang('TelegramPremium')} + {oldLang('TelegramPremium')} )} {shouldDisplayStars && ( @@ -186,7 +186,7 @@ const SettingsMain: FC = ({ // eslint-disable-next-line react/jsx-no-bind onClick={() => openStarsBalanceModal({})} > - {lang('MenuTelegramStars')} + {oldLang('MenuTelegramStars')} {Boolean(starsBalance) && ( {formatInteger(starsBalance)} )} @@ -199,7 +199,7 @@ const SettingsMain: FC = ({ // eslint-disable-next-line react/jsx-no-bind onClick={() => openPremiumGiftingModal()} > - {lang('GiftPremiumGifting')} + {oldLang('GiftPremiumGifting')} )}
@@ -209,7 +209,7 @@ const SettingsMain: FC = ({ narrow onClick={openSupportDialog} > - {lang('AskAQuestion')} + {oldLang('AskAQuestion')} = ({ // eslint-disable-next-line react/jsx-no-bind onClick={() => openUrl({ url: FAQ_URL })} > - {lang('TelegramFaq')} + {oldLang('TelegramFaq')} = ({ // eslint-disable-next-line react/jsx-no-bind onClick={() => openUrl({ url: PRIVACY_URL })} > - {lang('PrivacyPolicy')} + {oldLang('PrivacyPolicy')} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index e730d4e58..92ee90403 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -74,6 +74,7 @@ import ReactionPicker from '../middle/message/reactions/ReactionPicker.async'; import MessageListHistoryHandler from '../middle/MessageListHistoryHandler'; import MiddleColumn from '../middle/MiddleColumn'; import ModalContainer from '../modals/ModalContainer'; +import StarGiftInfoModal from '../modals/stars/StarGiftInfoModal'; import PaymentModal from '../payment/PaymentModal.async'; import ReceiptModal from '../payment/ReceiptModal.async'; import RightColumn from '../right/RightColumn'; @@ -92,8 +93,9 @@ import NewContactModal from './NewContactModal.async'; import Notifications from './Notifications.async'; import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async'; import GiveawayModal from './premium/GiveawayModal.async'; -import PremiumGiftingModal from './premium/PremiumGiftingModal.async'; +import PremiumGiftingPickerModal from './premium/PremiumGiftingPickerModal.async'; import PremiumMainModal from './premium/PremiumMainModal.async'; +import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async'; import SafeLinkModal from './SafeLinkModal.async'; import './Main.scss'; @@ -145,7 +147,9 @@ type StateProps = { isAppendModalOpen?: boolean; isGiveawayModalOpen?: boolean; isDeleteMessageModalOpen?: boolean; - isPremiumGiftingModalOpen?: boolean; + isPremiumGiftingPickerModal?: boolean; + isStarsGiftingPickerModal?: boolean; + isStarGiftInfoModal?: boolean; isCurrentUserPremium?: boolean; noRightColumnAnimation?: boolean; withInterfaceAnimations?: boolean; @@ -195,7 +199,9 @@ const Main: FC = ({ isPremiumModalOpen, isGiveawayModalOpen, isDeleteMessageModalOpen, - isPremiumGiftingModalOpen, + isPremiumGiftingPickerModal, + isStarsGiftingPickerModal, + isStarGiftInfoModal, isPaymentModalOpen, isReceiptModalOpen, isReactionPickerOpen, @@ -574,7 +580,9 @@ const Main: FC = ({ {isPremiumModalOpen && } {isGiveawayModalOpen && } - {isPremiumGiftingModalOpen && } + {isPremiumGiftingPickerModal && } + {isStarsGiftingPickerModal && } + {isStarGiftInfoModal && } @@ -616,6 +624,8 @@ export default memo(withGlobal( giveawayModal, deleteMessageModal, giftingModal, + starsGiftingModal, + starGiftInfoModal, isMasterTab, payment, limitReachedModal, @@ -671,7 +681,9 @@ export default memo(withGlobal( isPremiumModalOpen: premiumModal?.isOpen, isGiveawayModalOpen: giveawayModal?.isOpen, isDeleteMessageModalOpen: Boolean(deleteMessageModal), - isPremiumGiftingModalOpen: giftingModal?.isOpen, + isPremiumGiftingPickerModal: giftingModal?.isOpen, + isStarsGiftingPickerModal: starsGiftingModal?.isOpen, + isStarGiftInfoModal: starGiftInfoModal?.isOpen, limitReached: limitReachedModal?.limit, isPaymentModalOpen: payment.isPaymentModalOpen, isReceiptModalOpen: Boolean(payment.receipt), diff --git a/src/components/main/premium/GiftPremiumModal.module.scss b/src/components/main/premium/GiftPremiumModal.module.scss deleted file mode 100644 index 2b485dfae..000000000 --- a/src/components/main/premium/GiftPremiumModal.module.scss +++ /dev/null @@ -1,46 +0,0 @@ -@media (min-width: 451px) { - .modalDialog :global(.modal-dialog) { - max-width: 32rem !important; - } -} - -.closeButton { - position: absolute; - top: 0.5rem; - left: 0.5rem; -} - -.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; -} - -.boostIcon { - color: var(--color-primary); - vertical-align: middle; - line-height: 1.5; -} diff --git a/src/components/main/premium/GiftPremiumModal.async.tsx b/src/components/main/premium/PremiumGiftModal.async.tsx similarity index 51% rename from src/components/main/premium/GiftPremiumModal.async.tsx rename to src/components/main/premium/PremiumGiftModal.async.tsx index 22ff5d68e..8a2064148 100644 --- a/src/components/main/premium/GiftPremiumModal.async.tsx +++ b/src/components/main/premium/PremiumGiftModal.async.tsx @@ -1,18 +1,18 @@ import type { FC } from '../../../lib/teact/teact'; import React from '../../../lib/teact/teact'; -import type { OwnProps } from './GiftPremiumModal'; +import type { OwnProps } from './PremiumGiftModal'; import { Bundles } from '../../../util/moduleLoader'; import useModuleLoader from '../../../hooks/useModuleLoader'; -const GiftPremiumModalAsync: FC = (props) => { +const PremiumGiftModalAsync: FC = (props) => { const { isOpen } = props; - const GiftPremiumModal = useModuleLoader(Bundles.Extra, 'GiftPremiumModal', !isOpen); + const PremiumGiftModal = useModuleLoader(Bundles.Extra, 'PremiumGiftModal', !isOpen); // eslint-disable-next-line react/jsx-props-no-spreading - return GiftPremiumModal ? : undefined; + return PremiumGiftModal ? : undefined; }; -export default GiftPremiumModalAsync; +export default PremiumGiftModalAsync; diff --git a/src/components/main/premium/PremiumGiftModal.module.scss b/src/components/main/premium/PremiumGiftModal.module.scss new file mode 100644 index 000000000..7b5f0ef14 --- /dev/null +++ b/src/components/main/premium/PremiumGiftModal.module.scss @@ -0,0 +1,123 @@ +@use '../../../styles/mixins'; + +@media (min-width: 451px) { + .modalDialog :global(.modal-dialog) { + max-width: 32rem !important; + } +} + +.root { + z-index: calc(var(--z-media-viewer) - 1); +} + +.root :global(.modal-content) { + padding: 0; +} + +.root :global(.modal-dialog) { + height: min(calc(55vh + 41px + 193px), 90vh); + max-width: 26.25rem; +} + +.root :global(.modal-dialog), +.root :global(.modal-content), +.transition { + overflow: hidden; +} + +.content { + display: flex; + flex-direction: column; +} + +.main { + overflow-y: scroll; +} + +.giftSection { + padding: 1.5rem; +} + +.section { + padding: 0.5rem; +} + +.header { + z-index: 2; + display: flex; + align-items: center; + border-bottom: 0.0625rem solid var(--color-borders); + position: absolute; + width: 100%; + left: 0; + top: 0; + height: 3.5rem; + padding: 0.5rem; + background: var(--color-background); + transition: 0.25s ease-out transform; +} + +.starHeaderText { + font-size: 1.25rem; + font-weight: 500; + margin: 0 0 0 3rem; + unicode-bidi: plaintext; +} + +.hiddenHeader { + transform: translateY(-100%); +} + +.closeButton { + position: absolute; + top: 0.5rem; + left: 0.5rem; + z-index: 3; +} + +.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); +} + +.boostIcon { + color: var(--color-primary); + vertical-align: middle; + line-height: 1.5; +} + +.optionBottom { + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.secondaryInfo { + text-align: center; + font-size: 0.875rem; + color: var(--color-text-secondary); + padding: 0.5rem 1rem; +} + +.footer { + margin: 0 1.5rem; +} diff --git a/src/components/main/premium/GiftPremiumModal.tsx b/src/components/main/premium/PremiumGiftModal.tsx similarity index 81% rename from src/components/main/premium/GiftPremiumModal.tsx rename to src/components/main/premium/PremiumGiftModal.tsx index c4f9d9892..a3c42c883 100644 --- a/src/components/main/premium/GiftPremiumModal.tsx +++ b/src/components/main/premium/PremiumGiftModal.tsx @@ -28,7 +28,7 @@ import Link from '../../ui/Link'; import Modal from '../../ui/Modal'; import PremiumSubscriptionOption from './PremiumSubscriptionOption'; -import styles from './GiftPremiumModal.module.scss'; +import styles from './PremiumGiftModal.module.scss'; export type OwnProps = { isOpen?: boolean; @@ -41,7 +41,7 @@ type StateProps = { boostPerSentGift?: number; }; -const GiftPremiumModal: FC = ({ +const PremiumGiftModal: FC = ({ isOpen, isCompleted, gifts, @@ -52,10 +52,11 @@ const GiftPremiumModal: FC = ({ const dialogRef = useRef(null); const { - openPremiumModal, closeGiftPremiumModal, openInvoice, requestConfetti, + openPremiumModal, closePremiumGiftModal, openInvoice, requestConfetti, } = getActions(); - const lang = useOldLang(); + const oldLang = useOldLang(); + const [selectedMonthOption, setSelectedMonthOption] = useState(); const selectedUserQuantity = forUserIds && forUserIds.length * boostPerSentGift; @@ -140,23 +141,23 @@ const GiftPremiumModal: FC = ({ function renderGiftTitle() { if (isCompleted) { - return renderText(lang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle', + return renderText(oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle', [userNameList, selectedGift?.months]), ['simple_markdown']); } - return lang('GiftTelegramPremiumTitle'); + return oldLang('GiftTelegramPremiumTitle'); } function renderGiftText() { if (isCompleted) { - return renderText(lang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', userNameList), + return renderText(oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', userNameList), ['simple_markdown']); } - return renderText(lang('GiftPremiumUsersGiveAccessManyZero', userNameList), ['simple_markdown']); + return renderText(oldLang('GiftPremiumUsersGiveAccessManyZero', userNameList), ['simple_markdown']); } function renderPremiumFeaturesLink() { - const info = lang('GiftPremiumListFeaturesAndTerms'); + const info = oldLang('GiftPremiumListFeaturesAndTerms'); // Translation hack for rendering component inside string const parts = info.match(/([^*]*)\*([^*]+)\*(.*)/); @@ -174,7 +175,8 @@ const GiftPremiumModal: FC = ({ } function renderBoostsPluralText() { - const giftParts = renderText(lang('GiftPremiumWillReceiveBoostsPlural', selectedUserQuantity), ['simple_markdown']); + const giftParts = renderText(oldLang('GiftPremiumWillReceiveBoostsPlural', + selectedUserQuantity), ['simple_markdown']); return giftParts.map((part) => { if (typeof part === 'string') { return part.split(/(⚡)/g).map((subpart) => { @@ -210,19 +212,20 @@ const GiftPremiumModal: FC = ({ return ( -
+
@@ -244,7 +247,7 @@ const GiftPremiumModal: FC = ({ {renderText(renderBoostsPluralText(), ['simple_markdown', 'emoji'])}

-
+
{renderSubscriptionGiftOptions()}
@@ -253,12 +256,14 @@ const GiftPremiumModal: FC = ({
{!isCompleted && ( - +
+ +
)} ); @@ -267,7 +272,7 @@ const GiftPremiumModal: FC = ({ export default memo(withGlobal((global): StateProps => { const { gifts, forUserIds, isCompleted, - } = selectTabState(global).giftPremiumModal || {}; + } = selectTabState(global).giftModal || {}; return { isCompleted, @@ -275,4 +280,4 @@ export default memo(withGlobal((global): StateProps => { boostPerSentGift: global.appConfig?.boostsPerSentGift, forUserIds, }; -})(GiftPremiumModal)); +})(PremiumGiftModal)); diff --git a/src/components/main/premium/PremiumGiftingModal.async.tsx b/src/components/main/premium/PremiumGiftingModal.async.tsx deleted file mode 100644 index c297565d1..000000000 --- a/src/components/main/premium/PremiumGiftingModal.async.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { FC } from '../../../lib/teact/teact'; -import React from '../../../lib/teact/teact'; - -import type { OwnProps } from './PremiumGiftingModal'; - -import { Bundles } from '../../../util/moduleLoader'; - -import useModuleLoader from '../../../hooks/useModuleLoader'; - -const PremiumGiftingModalAsync: FC = (props) => { - const { isOpen } = props; - const PremiumGiftingModal = useModuleLoader(Bundles.Extra, 'PremiumGiftingModal', !isOpen); - - // eslint-disable-next-line react/jsx-props-no-spreading - return PremiumGiftingModal ? : undefined; -}; - -export default PremiumGiftingModalAsync; diff --git a/src/components/main/premium/PremiumGiftingPickerModal.async.tsx b/src/components/main/premium/PremiumGiftingPickerModal.async.tsx new file mode 100644 index 000000000..1db4183a4 --- /dev/null +++ b/src/components/main/premium/PremiumGiftingPickerModal.async.tsx @@ -0,0 +1,18 @@ +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 = (props) => { + const { isOpen } = props; + const PremiumGiftingPickerModal = useModuleLoader(Bundles.Extra, 'PremiumGiftingPickerModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return PremiumGiftingPickerModal ? : undefined; +}; + +export default PremiumGiftingPickerModalAsync; diff --git a/src/components/main/premium/PremiumGiftingModal.module.scss b/src/components/main/premium/PremiumGiftingPickerModal.module.scss similarity index 100% rename from src/components/main/premium/PremiumGiftingModal.module.scss rename to src/components/main/premium/PremiumGiftingPickerModal.module.scss diff --git a/src/components/main/premium/PremiumGiftingModal.tsx b/src/components/main/premium/PremiumGiftingPickerModal.tsx similarity index 53% rename from src/components/main/premium/PremiumGiftingModal.tsx rename to src/components/main/premium/PremiumGiftingPickerModal.tsx index f90aaf109..3f0fb7ac3 100644 --- a/src/components/main/premium/PremiumGiftingModal.tsx +++ b/src/components/main/premium/PremiumGiftingPickerModal.tsx @@ -8,19 +8,16 @@ import { GIVEAWAY_MAX_ADDITIONAL_CHANNELS } from '../../../config'; import { filterUsersByName, isUserBot, } from '../../../global/helpers'; -import buildClassName from '../../../util/buildClassName'; import { unique } from '../../../util/iteratees'; import sortChatIds from '../../common/helpers/sortChatIds'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; -import Icon from '../../common/icons/Icon'; import PeerPicker from '../../common/pickers/PeerPicker'; -import Button from '../../ui/Button'; -import Modal from '../../ui/Modal'; +import PickerModal from '../../common/pickers/PickerModal'; -import styles from './PremiumGiftingModal.module.scss'; +import styles from './PremiumGiftingPickerModal.module.scss'; export type OwnProps = { isOpen?: boolean; @@ -32,15 +29,15 @@ interface StateProps { userIds?: string[]; } -const PremiumGiftingModal: FC = ({ +const PremiumGiftingPickerModal: FC = ({ isOpen, currentUserId, userSelectionLimit = GIVEAWAY_MAX_ADDITIONAL_CHANNELS, userIds, }) => { - const { closePremiumGiftingModal, openGiftPremiumModal, showNotification } = getActions(); + const { closePremiumGiftingModal, openPremiumGiftModal, showNotification } = getActions(); - const lang = useOldLang(); + const oldLang = useOldLang(); const [selectedUserIds, setSelectedUserIds] = useState([]); const [searchQuery, setSearchQuery] = useState(''); @@ -61,8 +58,7 @@ const PremiumGiftingModal: FC = ({ const handleSendIdList = useLastCallback(() => { if (selectedUserIds?.length) { - openGiftPremiumModal({ forUserIds: selectedUserIds }); - + openPremiumGiftModal({ forUserIds: selectedUserIds }); closePremiumGiftingModal(); } }); @@ -70,65 +66,42 @@ const PremiumGiftingModal: FC = ({ const handleSelectedUserIdsChange = useLastCallback((newSelectedIds: string[]) => { if (newSelectedIds.length > userSelectionLimit) { showNotification({ - message: lang('BoostingSelectUpToWarningUsers', userSelectionLimit), + message: oldLang('BoostingSelectUpToWarningUsers', userSelectionLimit), }); return; } setSelectedUserIds(newSelectedIds); }); - function renderSearchField() { - return ( -
- -

{lang('GiftTelegramPremiumTitle')} -

-
- ); - } - return ( - -
- {renderSearchField()} -
- -
-
- -
-
-
+ + ); }; @@ -140,4 +113,4 @@ export default memo(withGlobal((global): StateProps => { userIds: global.contactList?.userIds, userSelectionLimit: global.appConfig?.giveawayAddPeersMax, }; -})(PremiumGiftingModal)); +})(PremiumGiftingPickerModal)); diff --git a/src/components/main/premium/StarsGiftModal.async.tsx b/src/components/main/premium/StarsGiftModal.async.tsx new file mode 100644 index 000000000..fd9a076ee --- /dev/null +++ b/src/components/main/premium/StarsGiftModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './StarsGiftModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const StarsGiftModalAsync: FC = (props) => { + const { isOpen } = props; + const StarsGiftModal = useModuleLoader(Bundles.Extra, 'StarsGiftModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return StarsGiftModal ? : undefined; +}; + +export default StarsGiftModalAsync; diff --git a/src/components/main/premium/StarsGiftModal.module.scss b/src/components/main/premium/StarsGiftModal.module.scss new file mode 100644 index 000000000..86f3a66a0 --- /dev/null +++ b/src/components/main/premium/StarsGiftModal.module.scss @@ -0,0 +1,107 @@ +@media (min-width: 451px) { + .modalDialog :global(.modal-dialog) { + max-width: 32rem !important; + } +} + +.root { + z-index: calc(var(--z-media-viewer) - 1); +} + +.root :global(.modal-content) { + padding: 0; +} + +.root :global(.modal-dialog) { + height: min(calc(55vh + 41px + 193px), 90vh); + max-width: 26.25rem; +} + +.root :global(.modal-dialog), +.root :global(.modal-content), +.transition { + overflow: hidden; +} + +.main { + height: 100%; + overflow-y: scroll; + display: flex; + flex-direction: column; +} + +.section { + padding: 0.5rem; +} + +.header { + z-index: 2; + display: flex; + align-items: center; + border-bottom: 0.0625rem solid var(--color-borders); + position: absolute; + width: 100%; + left: 0; + top: 0; + height: 3.5rem; + padding: 0.5rem; + background: var(--color-background); + transition: 0.25s ease-out transform; +} + +.starHeaderText { + font-size: 1.25rem; + font-weight: 500; + margin: 0 0 0 3.5rem; + unicode-bidi: plaintext; +} + +.hiddenHeader { + transform: translateY(-100%); +} + +.closeButton { + position: absolute; + top: 0.5rem; + left: 0.5rem; + z-index: 3; +} + +.avatars { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 1rem; + margin: 1rem; +} + +.center { + text-align: center; +} + +.options { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + width: 100%; + margin-bottom: 2.5rem; +} + +.boostIcon { + color: var(--color-primary); + vertical-align: middle; + line-height: 1.5; +} + +.moreOptions { + grid-column: 1/-1; +} + +.secondaryInfo { + text-align: center; + font-size: 0.875rem; + color: var(--color-text-secondary); + padding: 0.5rem 1rem; + margin-top: auto; +} diff --git a/src/components/main/premium/StarsGiftModal.tsx b/src/components/main/premium/StarsGiftModal.tsx new file mode 100644 index 000000000..4aada44f7 --- /dev/null +++ b/src/components/main/premium/StarsGiftModal.tsx @@ -0,0 +1,197 @@ +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 { + ApiStarsGiftOption, ApiStarTopupOption, ApiUser, +} from '../../../api/types'; + +import { + selectTabState, 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 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 styles from './StarsGiftModal.module.scss'; + +export type OwnProps = { + isOpen?: boolean; +}; + +type StateProps = { + isCompleted?: boolean; + starsGiftOptions?: ApiStarsGiftOption[] | undefined; + forUserId?: string; + user?: ApiUser; +}; + +const StarsGiftModal: FC = ({ + isOpen, + isCompleted, + starsGiftOptions, + forUserId, + user, +}) => { + // eslint-disable-next-line no-null/no-null + const dialogRef = useRef(null); + + const { + closeStarsGiftModal, openInvoice, requestConfetti, + } = getActions(); + + const oldLang = useOldLang(); + + const [selectedOption, setSelectedOption] = useState(); + const [isHeaderHidden, setHeaderHidden] = useState(true); + + useEffect(() => { + if (!isOpen) { + setHeaderHidden(true); + } + }, [isOpen]); + + 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 handleClick = useLastCallback((option: ApiStarTopupOption) => { + setSelectedOption(option); + openInvoice({ + type: 'starsgift', + userId: forUserId!, + stars: option.stars, + currency: option.currency, + amount: option.amount, + }); + }); + + function handleScroll(e: React.UIEvent) { + const { scrollTop } = e.currentTarget; + + setHeaderHidden(scrollTop <= 150); + } + + function renderGiftTitle() { + if (isCompleted) { + return renderText(oldLang('Notification.StarsGift.SentYou', + formatCurrencyAsString(selectedOption!.amount, selectedOption!.currency, oldLang.code)), ['simple_markdown']); + } + + return oldLang('GiftStarsTitle'); + } + + function renderStarOptionList() { + return ( + + ); + } + + const bottomText = useMemo(() => { + if (!isOpen) return undefined; + + const text = oldLang('lng_credits_summary_options_about'); + const parts = text.split('{link}'); + return [ + parts[0], + , + parts[1], + ]; + }, [isOpen, oldLang]); + + return ( + +
+ +
+

+ {oldLang('GiftStarsTitle')} +

+
+
+ +
+

+ {renderGiftTitle()} +

+ {!isCompleted && ( + <> +
+ {renderStarOptionList()} +
+
+ {bottomText} +
+ + )} +
+
+ ); +}; + +export default memo(withGlobal((global): StateProps => { + const { + starsGiftOptions, forUserId, isCompleted, + } = selectTabState(global).starsGiftModal || {}; + + const user = forUserId ? selectUser(getGlobal(), forUserId) : undefined; + + return { + isCompleted, + starsGiftOptions, + forUserId, + user, + }; +})(StarsGiftModal)); diff --git a/src/components/main/premium/StarsGiftingPickerModal.async.tsx b/src/components/main/premium/StarsGiftingPickerModal.async.tsx new file mode 100644 index 000000000..45e020acf --- /dev/null +++ b/src/components/main/premium/StarsGiftingPickerModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './StarsGiftingPickerModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const StarsGiftingPickerModalAsync: FC = (props) => { + const { isOpen } = props; + const StarsGiftingPickerModal = useModuleLoader(Bundles.Extra, 'StarsGiftingPickerModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return StarsGiftingPickerModal ? : undefined; +}; + +export default StarsGiftingPickerModalAsync; diff --git a/src/components/main/premium/StarsGiftingPickerModal.module.scss b/src/components/main/premium/StarsGiftingPickerModal.module.scss new file mode 100644 index 000000000..f98e17843 --- /dev/null +++ b/src/components/main/premium/StarsGiftingPickerModal.module.scss @@ -0,0 +1,86 @@ +.root { + z-index: calc(var(--z-media-viewer) - 1); +} + +.root :global(.modal-content) { + padding: 0; +} + +.root :global(.modal-dialog) { + max-width: 55vh; +} + +.root :global(.modal-dialog), +.root :global(.modal-content), +.transition { + 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; +} diff --git a/src/components/main/premium/StarsGiftingPickerModal.tsx b/src/components/main/premium/StarsGiftingPickerModal.tsx new file mode 100644 index 000000000..d9d0477f8 --- /dev/null +++ b/src/components/main/premium/StarsGiftingPickerModal.tsx @@ -0,0 +1,141 @@ +import type { FC } from '../../../lib/teact/teact'; +import React, { + memo, useMemo, + useRef, + useState, +} from '../../../lib/teact/teact'; +import { getActions, getGlobal, withGlobal } from '../../../global'; + +import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; +import { + filterUsersByName, isDeletedUser, isUserBot, +} from '../../../global/helpers'; +import buildClassName from '../../../util/buildClassName'; +import { unique } from '../../../util/iteratees'; +import sortChatIds from '../../common/helpers/sortChatIds'; + +import useLastCallback from '../../../hooks/useLastCallback'; +import useOldLang from '../../../hooks/useOldLang'; + +import Icon from '../../common/icons/Icon'; +import PeerPicker from '../../common/pickers/PeerPicker'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; + +import styles from './StarsGiftingPickerModal.module.scss'; + +export type OwnProps = { + isOpen?: boolean; +}; + +interface StateProps { + currentUserId?: string; + userIds?: string[]; + activeListIds?: string[]; + archivedListIds?: string[]; +} + +const StarsGiftingPickerModal: FC = ({ + isOpen, + currentUserId, + activeListIds, + archivedListIds, + userIds, +}) => { + // eslint-disable-next-line no-null/no-null + const dialogRef = useRef(null); + const { closeStarsGiftingModal, openStarsGiftModal } = getActions(); + + const oldLang = useOldLang(); + + const [searchQuery, setSearchQuery] = useState(''); + + const displayedUserIds = useMemo(() => { + const usersById = getGlobal().users.byId; + const combinedIds = [ + ...(userIds || []), + ...(activeListIds || []), + ...(archivedListIds || []), + ]; + + const filteredContactIds = filterUsersByName(combinedIds, usersById, searchQuery); + + return sortChatIds(unique(filteredContactIds).filter((id) => { + const user = usersById[id]; + + if (!user) { + return false; + } + + return !user.isSupport + && !isUserBot(user) && !isDeletedUser(user) + && id !== currentUserId && id !== SERVICE_NOTIFICATIONS_USER_ID; + })); + }, [currentUserId, searchQuery, userIds, activeListIds, archivedListIds]); + + const handleSelectedUserIdsChange = useLastCallback((newSelectedId?: string) => { + if (newSelectedId?.length) { + openStarsGiftModal({ forUserId: newSelectedId }); + } + }); + + function renderHeaderText() { + return ( +
+ +

{oldLang('GiftStarsTitle')} +

+
+ ); + } + + return ( + +
+ {renderHeaderText()} + +
+
+ ); +}; + +export default memo(withGlobal((global): StateProps => { + const { + chats: { + listIds, + }, + currentUserId, + } = global; + + return { + userIds: global.contactList?.userIds, + activeListIds: listIds.active, + archivedListIds: listIds.archived, + currentUserId, + }; +})(StarsGiftingPickerModal)); diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index d68efa835..22d666f11 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -13,7 +13,7 @@ import type { FocusDirection, ThreadId } from '../../types'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import { - getChatTitle, getMessageHtmlId, isJoinedChannelMessage, + getChatTitle, getMessageHtmlId, getSenderTitle, isJoinedChannelMessage, } from '../../global/helpers'; import { getMessageReplyInfo } from '../../global/helpers/replies'; import { @@ -35,6 +35,7 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useEnsureMessage from '../../hooks/useEnsureMessage'; import useFlag from '../../hooks/useFlag'; import { useIsIntersecting, useOnIntersect } from '../../hooks/useIntersectionObserver'; +import useLang from '../../hooks/useLang'; import useOldLang from '../../hooks/useOldLang'; import useShowTransition from '../../hooks/useShowTransition'; import useFocusMessage from './message/hooks/useFocusMessage'; @@ -103,10 +104,11 @@ const ActionMessage: FC = ({ onPinnedIntersectionChange, }) => { const { - openPremiumModal, requestConfetti, checkGiftCode, getReceipt, + openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openStarGiftInfoModal, } = getActions(); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); @@ -133,6 +135,7 @@ const ActionMessage: FC = ({ const isGiftCode = Boolean(message.content.action?.text.startsWith('BoostingReceivedGift')); const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo; const isJoinedMessage = isJoinedChannelMessage(message); + const hasStars = Boolean(message.content.action?.stars); useEffect(() => { if (noAppearanceAnimation) { @@ -168,7 +171,7 @@ const ActionMessage: FC = ({ const renderContent = useCallback(() => { return renderActionMessageText( - lang, + oldLang, message, senderUser, senderChat, @@ -181,7 +184,7 @@ const ActionMessage: FC = ({ observeIntersectionForPlaying, ); }, [ - isEmbedded, lang, message, observeIntersectionForLoading, observeIntersectionForPlaying, + isEmbedded, message, observeIntersectionForLoading, observeIntersectionForPlaying, oldLang, senderChat, senderUser, targetChatId, targetMessage, targetUsers, topic, ]); @@ -197,6 +200,14 @@ const ActionMessage: FC = ({ handleBeforeContextMenu(e); }; + const handleStarGiftClick = () => { + openStarGiftInfoModal({ + toUserId: targetUserIds?.[0], + stars: message.content.action!.stars, + date: message.date, + }); + }; + const handlePremiumGiftClick = () => { openPremiumModal({ isGift: true, @@ -233,7 +244,12 @@ const ActionMessage: FC = ({ function renderGift() { return ( - + = ({ noLoop nonInteractive /> - {lang('ActionGiftPremiumTitle')} - {lang('ActionGiftPremiumSubtitle', lang('Months', message.content.action?.months, 'i'))} + {hasStars ? oldLang('Stars', message.content.action?.stars) + : oldLang('ActionGiftPremiumTitle')} + + {hasStars ? oldLang('ActionGiftStarsSubtitleYou') + : oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))} + - {lang('ActionGiftPremiumView')} + {oldLang('ActionGiftPremiumView')} ); } @@ -257,7 +277,7 @@ const ActionMessage: FC = ({ className="action-message-gift action-message-gift-code" tabIndex={0} role="button" - onClick={handleGiftCodeClick} + onClick={hasStars ? handleStarGiftClick : handleGiftCodeClick} > = ({ noLoop nonInteractive /> - {lang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')} + {hasStars ? oldLang('Stars', message.content.action?.stars) + : oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')} + - {targetChat && renderText(lang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed + {hasStars ? lang('GiftStarsOutgoing', { + user: ( + + {senderUser && renderText(getSenderTitle(oldLang, senderUser) || '', ['simple_markdown'])} + + ), + }, { + withNodes: true, + }) : targetChat && renderText(oldLang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', - getChatTitle(lang, targetChat)), + getChatTitle(oldLang, targetChat)), ['simple_markdown'])} - - {renderText(lang( - 'BoostingUnclaimedPrizeDuration', - lang('Months', message.content.action?.months, 'i'), - ), ['simple_markdown'])} - + {!hasStars && ( + + {renderText(oldLang( + 'BoostingUnclaimedPrizeDuration', + oldLang('Months', message.content.action?.months, 'i'), + ), ['simple_markdown'])} + + )} - {lang('BoostingReceivedGiftOpenBtn')} + { + oldLang(hasStars ? 'ActionGiftPremiumView' : 'BoostingReceivedGiftOpenBtn') + } + ); } diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index f315b4aaa..8618c95ce 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -187,7 +187,7 @@ const HeaderMenuContainer: FC = ({ requestMasterAndRequestCall, toggleStatistics, openBoostStatistics, - openGiftPremiumModal, + openPremiumGiftModal, openThreadWithInfo, openCreateTopicPanel, openEditTopicPanel, @@ -313,7 +313,7 @@ const HeaderMenuContainer: FC = ({ }); const handleGiftPremiumClick = useLastCallback(() => { - openGiftPremiumModal({ forUserIds: [chatId] }); + openPremiumGiftModal({ forUserIds: [chatId] }); closeMenu(); }); diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss index 258d806af..effcdfcd1 100644 --- a/src/components/middle/MessageList.scss +++ b/src/components/middle/MessageList.scss @@ -277,6 +277,11 @@ margin-inline: auto; } + .action-message-stars-gift { + width: 15rem; + margin-inline: auto; + } + .action-message-subtitle { margin-top: 1rem; font-weight: normal; diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 18a925653..81fa046bd 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -82,7 +82,8 @@ import Composer from '../common/Composer'; import PrivacySettingsNoticeModal from '../common/PrivacySettingsNoticeModal.async'; import SeenByModal from '../common/SeenByModal.async'; import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async'; -import GiftPremiumModal from '../main/premium/GiftPremiumModal.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'; @@ -133,7 +134,8 @@ type StateProps = { isSeenByModalOpen: boolean; isPrivacySettingsNoticeModalOpen: boolean; isReactorListModalOpen: boolean; - isGiftPremiumModalOpen?: boolean; + isPremiumGiftModalOpen?: boolean; + isStarsGiftModalOpen?: boolean; isChatLanguageModalOpen?: boolean; withInterfaceAnimations?: boolean; shouldSkipHistoryAnimations?: boolean; @@ -193,7 +195,8 @@ function MiddleColumn({ isSeenByModalOpen, isPrivacySettingsNoticeModalOpen, isReactorListModalOpen, - isGiftPremiumModalOpen, + isPremiumGiftModalOpen, + isStarsGiftModalOpen, isChatLanguageModalOpen, withInterfaceAnimations, shouldSkipHistoryAnimations, @@ -715,7 +718,8 @@ function MiddleColumn({ /> ))}
- + +
); } @@ -729,7 +733,7 @@ export default memo(withGlobal( const { messageLists, isLeftColumnShown, activeEmojiInteractions, - seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations, + seenByModal, giftModal, starsGiftModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations, chatLanguageModal, privacySettingsNoticeModal, } = selectTabState(global); const currentMessageList = selectCurrentMessageList(global); @@ -748,7 +752,8 @@ export default memo(withGlobal( isSeenByModalOpen: Boolean(seenByModal), isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal), isReactorListModalOpen: Boolean(reactorModal), - isGiftPremiumModalOpen: giftPremiumModal?.isOpen, + isPremiumGiftModalOpen: giftModal?.isOpen, + isStarsGiftModalOpen: starsGiftModal?.isOpen, isChatLanguageModalOpen: Boolean(chatLanguageModal), withInterfaceAnimations: selectCanAnimateInterface(global), currentTransitionKey: Math.max(0, messageLists.length - 1), diff --git a/src/components/modals/common/TableInfoModal.module.scss b/src/components/modals/common/TableInfoModal.module.scss index 19cb3ead6..8b123098c 100644 --- a/src/components/modals/common/TableInfoModal.module.scss +++ b/src/components/modals/common/TableInfoModal.module.scss @@ -1,3 +1,5 @@ +@use '../../../styles/mixins'; + .content { display: flex; flex-direction: column; @@ -19,10 +21,30 @@ padding: 0.25rem 0.5rem; } +.section { + display: flex; + flex-direction: column; + align-items: center; + + padding: 0.5rem; + position: relative; + + @include mixins.adapt-padding-to-scrollbar(0.5rem); +} + .logo { + margin: 1rem; width: 6.25rem; height: 6.25rem; - align-self: center; + min-height: 6.25rem; +} + +.logoBackground { + position: absolute; + top: 0.75rem; + left: 50%; + transform: translateX(-50%); + height: 8rem; } .avatar { diff --git a/src/components/modals/common/TableInfoModal.tsx b/src/components/modals/common/TableInfoModal.tsx index 69ba146d3..201c7dfc9 100644 --- a/src/components/modals/common/TableInfoModal.tsx +++ b/src/components/modals/common/TableInfoModal.tsx @@ -15,6 +15,8 @@ import Modal from '../../ui/Modal'; import styles from './TableInfoModal.module.scss'; +import StarsBackground from '../../../assets/stars-bg.png'; + type ChatItem = { chatId: string }; export type TableData = [TeactNode, TeactNode | ChatItem][]; @@ -24,6 +26,7 @@ type OwnProps = { title?: string; tableData?: TableData; headerImageUrl?: string; + logoBackground?: string; headerAvatarPeer?: ApiPeer | CustomPeer; headerAvatarWebPhoto?: ApiWebDocument; noHeaderImage?: boolean; @@ -40,6 +43,7 @@ const TableInfoModal = ({ title, tableData, headerImageUrl, + logoBackground, headerAvatarPeer, headerAvatarWebPhoto, noHeaderImage, @@ -73,7 +77,11 @@ const TableInfoModal = ({ withAvatar ? ( ) : ( - +
+ + {Boolean(logoBackground) + && } +
) )} {header} @@ -98,7 +106,7 @@ const TableInfoModal = ({ {footer} {buttonText && ( - + )}
); diff --git a/src/components/modals/stars/StarGiftInfoModal.async.tsx b/src/components/modals/stars/StarGiftInfoModal.async.tsx new file mode 100644 index 000000000..cab07ee85 --- /dev/null +++ b/src/components/modals/stars/StarGiftInfoModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './StarGiftInfoModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const StarGiftInfoModalAsync: FC = (props) => { + const { isOpen } = props; + const StarGiftInfoModal = useModuleLoader(Bundles.Extra, 'StarGiftInfoModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return StarGiftInfoModal ? : undefined; +}; + +export default StarGiftInfoModalAsync; diff --git a/src/components/modals/stars/StarGiftInfoModal.module.scss b/src/components/modals/stars/StarGiftInfoModal.module.scss new file mode 100644 index 000000000..625ba7883 --- /dev/null +++ b/src/components/modals/stars/StarGiftInfoModal.module.scss @@ -0,0 +1,32 @@ +@use '../../../styles/mixins'; + +.centered { + text-align: center !important; +} + +.section { + display: flex; + flex-direction: column; + align-items: center; + + padding: 0.5rem; + position: relative; + + @include mixins.adapt-padding-to-scrollbar(0.5rem); +} + +.info { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 0.5rem; +} + +.starTitle { + margin: 0; +} + +.footer { + margin: 1rem 0; + color: var(--color-text-secondary); +} diff --git a/src/components/modals/stars/StarGiftInfoModal.tsx b/src/components/modals/stars/StarGiftInfoModal.tsx new file mode 100644 index 000000000..22f62cd97 --- /dev/null +++ b/src/components/modals/stars/StarGiftInfoModal.tsx @@ -0,0 +1,150 @@ +import React, { memo, useMemo } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { ApiPeer } from '../../../api/types'; + +import { getSenderTitle } from '../../../global/helpers'; +import { selectTabState, selectUser } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; +import { formatDateTimeToString } from '../../../util/dates/dateFormat'; +import renderText from '../../common/helpers/renderText'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useOldLang from '../../../hooks/useOldLang'; + +import StarIcon from '../../common/icons/StarIcon'; +import SafeLink from '../../common/SafeLink'; +import TableInfoModal, { type TableData } from '../common/TableInfoModal'; + +import styles from './StarGiftInfoModal.module.scss'; + +import StarLogo from '../../../assets/icons/StarLogo.svg'; +import StarsBackground from '../../../assets/stars-bg.png'; + +export type OwnProps = { + isOpen?: boolean; +}; + +export type StateProps = { + stars?: number; + user?: ApiPeer; + date?: number; +}; + +const StarGiftInfoModal = ({ + isOpen, + stars, + user, + date, +}: OwnProps & StateProps) => { + const { + closeStarGiftInfoModal, + } = getActions(); + const oldLang = useOldLang(); + const lang = useLang(); + + const infoText = useMemo(() => { + const linkText = oldLang('GiftStarsSubtitleLinkName'); + + return lang('CreditsBoxHistoryEntryGiftOutAbout', + { + user: ( + + {user && renderText(getSenderTitle(oldLang, user) || '', ['simple_markdown'])} + + ), + link: ( + + ), + }, + { + withNodes: true, + }); + }, [lang, oldLang, user]); + + const footerText = useMemo(() => { + const linkText = oldLang('lng_payments_terms_link'); + return lang('CreditsBoxOutAbout', { + link: ( + + ), + }, { + withNodes: true, + }); + }, [lang, oldLang]); + + const handleButtonClick = useLastCallback(() => { + closeStarGiftInfoModal(); + }); + + const modalData = useMemo(() => { + if (!isOpen) return undefined; + + const header = ( + <> +

{oldLang('StarsGiftSent')}

+
+

{stars}

+ +
+

{infoText}

+ + ); + + const tableData = [ + [oldLang('Recipient'), user ? { chatId: user.id } : oldLang('BoostingNoRecipient')], + [oldLang('BoostingDate'), formatDateTimeToString(date! * 1000, lang.code, true)], + ] satisfies TableData; + + const footer = ( + + {footerText} + + ); + + return { + header, + tableData, + footer, + }; + }, [isOpen, oldLang, stars, infoText, user, date, lang.code, footerText]); + + if (!modalData) return undefined; + + return ( + + ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { + starGiftInfoModal, + } = selectTabState(global); + const toUserId = starGiftInfoModal?.toUserId; + const user = toUserId ? selectUser(global, toUserId) : undefined; + + return { + stars: starGiftInfoModal?.stars, + user, + date: starGiftInfoModal?.date, + }; + }, +)(StarGiftInfoModal)); diff --git a/src/components/modals/stars/StarTopupOptionList.module.scss b/src/components/modals/stars/StarTopupOptionList.module.scss new file mode 100644 index 000000000..1f4da3a47 --- /dev/null +++ b/src/components/modals/stars/StarTopupOptionList.module.scss @@ -0,0 +1,62 @@ +@use '../../../styles/mixins'; + +.option { + --_background-color: var(--color-background-secondary); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.125rem; + + padding: 1rem; + border-radius: 0.625rem; + + background-color: var(--_background-color); + transition: background-color 0.25s ease-out; + + cursor: var(--custom-cursor, pointer); + + &:hover { + --_background-color: var(--color-background-secondary-accent); + } +} + +.wideOption { + grid-column: 1 / -1; +} + +.optionTop { + display: flex; + align-items: center; + gap: 0.25rem; + + font-weight: 500; + font-size: 1.5rem; + line-height: 1; +} + +.stackedStars { + display: grid; + grid-auto-columns: 0.4375rem; + grid-auto-flow: column; + justify-items: end; +} + +.stackedStar { + @include mixins.filter-outline(0.0625rem, var(--_background-color)); + transition: filter 0.25s ease-out; +} + +.optionBottom { + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.moreOptions { + grid-column: 1/-1; +} + +.iconDown { + margin-inline-start: 0.25rem; + font-size: 1.5rem; +} diff --git a/src/components/modals/stars/StarTopupOptionList.tsx b/src/components/modals/stars/StarTopupOptionList.tsx new file mode 100644 index 000000000..eb70663c6 --- /dev/null +++ b/src/components/modals/stars/StarTopupOptionList.tsx @@ -0,0 +1,110 @@ +import type { FC } from '../../../lib/teact/teact'; +import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact'; + +import type { ApiStarTopupOption } from '../../../api/types'; + +import buildClassName from '../../../util/buildClassName'; +import { formatCurrency } from '../../../util/formatCurrency'; +import { formatInteger } from '../../../util/textFormat'; + +import useFlag from '../../../hooks/useFlag'; +import useOldLang from '../../../hooks/useOldLang'; + +import Icon from '../../common/icons/Icon'; +import StarIcon from '../../common/icons/StarIcon'; +import Button from '../../ui/Button'; + +import styles from './StarTopupOptionList.module.scss'; + +const MAX_STARS_COUNT = 6; + +type OwnProps = { + isActive?: boolean; + options?: ApiStarTopupOption[]; + starsNeeded?: number; + onClick: (option: ApiStarTopupOption) => void; +}; + +const StarTopupOptionList: FC = ({ + isActive, + options, + starsNeeded, + onClick, +}) => { + const lang = useOldLang(); + + const [areOptionsExtended, markOptionsExtended, unmarkOptionsExtended] = useFlag(); + + useEffect(() => { + if (!isActive) { + unmarkOptionsExtended(); + } + }, [isActive]); + + const [renderingOptions, canExtend] = useMemo(() => { + if (!options) { + return [undefined, false]; + } + + const maxOption = options.reduce((max, option) => ( + max.stars > option.stars ? max : option + )); + const forceShowAll = starsNeeded && maxOption.stars < starsNeeded; + + const result: { option: ApiStarTopupOption; starsCount: number; isWide: boolean }[] = []; + let currentStackedStarsCount = 0; + let canExtendOptions = false; + options.forEach((option, index) => { + if (!option.isExtended) currentStackedStarsCount++; + + if (starsNeeded && !forceShowAll && option.stars < starsNeeded) return; + if (!areOptionsExtended && option.isExtended) { + canExtendOptions = true; + return; + } + result.push({ + option, + starsCount: Math.min(currentStackedStarsCount, MAX_STARS_COUNT), + isWide: index === options.length - 1, + }); + }); + + return [result, canExtendOptions]; + }, [areOptionsExtended, options, starsNeeded]); + + return ( + <> + {renderingOptions?.map(({ option, starsCount, isWide }) => { + const length = renderingOptions?.length; + const isOdd = length % 2 === 0; + return ( +
onClick?.(option)} + > +
+ +{formatInteger(option.stars)} +
+ {Array.from({ length: starsCount }).map(() => ( + + ))} +
+
+
+ {formatCurrency(option.amount, option.currency, lang.code)} +
+
+ ); + })} + {!areOptionsExtended && canExtend && ( + + )} + + ); +}; + +export default memo(StarTopupOptionList); diff --git a/src/components/modals/stars/StarsBalanceModal.module.scss b/src/components/modals/stars/StarsBalanceModal.module.scss index d1b6f8eb8..0077575c8 100644 --- a/src/components/modals/stars/StarsBalanceModal.module.scss +++ b/src/components/modals/stars/StarsBalanceModal.module.scss @@ -24,6 +24,11 @@ overflow-y: scroll; } +.container { + display: flex; + flex-direction: column-reverse; +} + .section { display: flex; flex-direction: column; @@ -138,63 +143,16 @@ width: 100%; } -.option { - --_background-color: var(--color-background-secondary); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.125rem; - - padding: 1rem; - border-radius: 0.625rem; - - background-color: var(--_background-color); - transition: background-color 0.25s ease-out; - - cursor: var(--custom-cursor, pointer); - - &:hover { - --_background-color: var(--color-background-secondary-accent); - } +.optionFullWidth { + grid-column: 1 / -1; } -.optionTop { - display: flex; - align-items: center; - gap: 0.25rem; - - font-weight: 500; - font-size: 1.5rem; - line-height: 1; - white-space: nowrap; -} - -.stackedStars { - display: grid; - grid-auto-columns: 0.4375rem; - grid-auto-flow: column; - justify-items: center; - margin-left: 0.375rem; -} - -.stackedStar { - @include mixins.filter-outline(0.0625rem, var(--_background-color)); - transition: filter 0.25s ease-out; -} - -.optionBottom { - font-size: 0.875rem; - color: var(--color-text-secondary); -} - -.moreOptions { +.starButton { grid-column: 1/-1; -} - -.iconDown { - margin-inline-start: 0.25rem; - font-size: 1.5rem; + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; } .paymentContent { diff --git a/src/components/modals/stars/StarsBalanceModal.tsx b/src/components/modals/stars/StarsBalanceModal.tsx index 791610627..f46081e4b 100644 --- a/src/components/modals/stars/StarsBalanceModal.tsx +++ b/src/components/modals/stars/StarsBalanceModal.tsx @@ -7,13 +7,11 @@ import type { ApiStarTopupOption, ApiUser } from '../../../api/types'; import type { GlobalState, TabState } from '../../../global/types'; import { getUserFullName } from '../../../global/helpers'; -import { selectUser } from '../../../global/selectors'; +import { selectIsPremiumPurchaseBlocked, selectUser } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; -import { formatCurrency } from '../../../util/formatCurrency'; -import { formatInteger } from '../../../util/textFormat'; import renderText from '../../common/helpers/renderText'; -import useFlag from '../../../hooks/useFlag'; +import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; @@ -27,6 +25,7 @@ import TabList, { type TabWithProperties } from '../../ui/TabList'; import Transition from '../../ui/Transition'; import BalanceBlock from './BalanceBlock'; import TransactionItem from './StarsTransactionItem'; +import StarTopupOptionList from './StarTopupOptionList'; import styles from './StarsBalanceModal.module.scss'; @@ -39,7 +38,6 @@ const TRANSACTION_TABS: TabWithProperties[] = [ { title: 'StarsTransactionsIncoming' }, { title: 'StarsTransactionsOutgoing' }, ]; -const MAX_STARS_COUNT = 6; export type OwnProps = { modal: TabState['starsBalanceModal']; @@ -48,19 +46,22 @@ export type OwnProps = { type StateProps = { starsBalanceState?: GlobalState['stars']; originPaymentBot?: ApiUser; + canBuyPremium?: boolean; }; const StarsBalanceModal = ({ - modal, starsBalanceState, originPaymentBot, + modal, starsBalanceState, originPaymentBot, canBuyPremium, }: OwnProps & StateProps) => { - const { closeStarsBalanceModal, loadStarsTransactions, openInvoice } = getActions(); + const { + closeStarsBalanceModal, loadStarsTransactions, openInvoice, openStarsGiftingModal, + } = getActions(); const { balance, history, topupOptions } = starsBalanceState || {}; - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const [isHeaderHidden, setHeaderHidden] = useState(true); - const [areOptionsExtended, markOptionsExtended, unmarkOptionsExtended] = useFlag(); const [selectedTabIndex, setSelectedTabIndex] = useState(0); const isOpen = Boolean(modal && starsBalanceState); @@ -73,52 +74,21 @@ const StarsBalanceModal = ({ useEffect(() => { if (!isOpen) { setHeaderHidden(true); - unmarkOptionsExtended(); setSelectedTabIndex(0); } }, [isOpen]); - const [renderingOptions, canExtend] = useMemo(() => { - if (!topupOptions) { - return [undefined, false]; - } - - const maxOption = topupOptions.reduce((max, option) => ( - max.stars > option.stars ? max : option - )); - const forceShowAll = starsNeeded && maxOption.stars < starsNeeded; - - const result: { option: ApiStarTopupOption; starsCount: number }[] = []; - let currentStackedStarsCount = 0; - let canExtendOptions = false; - topupOptions.forEach((option) => { - if (!option.isExtended) currentStackedStarsCount++; - - if (starsNeeded && !forceShowAll && option.stars < starsNeeded) return; - if (!areOptionsExtended && option.isExtended) { - canExtendOptions = true; - return; - } - result.push({ - option, - starsCount: Math.min(currentStackedStarsCount, MAX_STARS_COUNT), - }); - }); - - return [result, canExtendOptions]; - }, [areOptionsExtended, topupOptions, starsNeeded]); - const tosText = useMemo(() => { if (!isOpen) return undefined; - const text = lang('lng_credits_summary_options_about'); + const text = oldLang('lng_credits_summary_options_about'); const parts = text.split('{link}'); return [ parts[0], - , + , parts[1], ]; - }, [isOpen, lang]); + }, [isOpen, oldLang]); function handleScroll(e: React.UIEvent) { const { scrollTop } = e.currentTarget; @@ -135,12 +105,27 @@ const StarsBalanceModal = ({ }); }); + function renderStarOptionList() { + return ( + + ); + } + const handleLoadMore = useLastCallback(() => { loadStarsTransactions({ type: TRANSACTION_TYPES[selectedTabIndex], }); }); + const openPremiumGiftingModalHandler = useLastCallback(() => { + openStarsGiftingModal({}); + }); + return (
@@ -158,29 +143,31 @@ const StarsBalanceModal = ({

- {lang('TelegramStars')} + {oldLang('TelegramStars')}

- {starsNeeded ? lang('StarsNeededTitle', starsNeeded) : lang('TelegramStars')} + {starsNeeded ? oldLang('StarsNeededTitle', starsNeeded) : oldLang('TelegramStars')}

{renderText( - starsNeeded ? lang('StarsNeededText', originBotName) : lang('TelegramStarsInfo'), + starsNeeded ? oldLang('StarsNeededText', originBotName) : oldLang('TelegramStarsInfo'), ['simple_markdown', 'emoji'], )}
- {renderingOptions?.map(({ option, starsCount }) => ( - - ))} - {!areOptionsExtended && canExtend && ( - )}
@@ -189,13 +176,7 @@ const StarsBalanceModal = ({ {tosText}
{shouldShowTransactions && ( - <> - +
- + +
)}
); }; -function StarTopupOption({ - option, starsCount, onClick, -}: { - option: ApiStarTopupOption; starsCount: number; onClick?: (option: ApiStarTopupOption) => void; -}) { - const lang = useOldLang(); - - return ( -
onClick?.(option)}> -
- +{formatInteger(option.stars)} - {/* Switch directionality for correct order. Can't use flex because https://issues.chromium.org/issues/40249030 */} -
- {Array.from({ length: starsCount }).map(() => ( - - ))} -
-
-
- {formatCurrency(option.amount, option.currency, lang.code)} -
-
- ); -} - export default memo(withGlobal( (global, { modal }): StateProps => { const botId = modal?.originPayment?.botId; @@ -259,6 +221,7 @@ export default memo(withGlobal( return { starsBalanceState: global.stars, originPaymentBot: bot, + canBuyPremium: !selectIsPremiumPurchaseBlocked(global), }; }, )(StarsBalanceModal)); diff --git a/src/config.ts b/src/config.ts index 76eaf87e6..495c39082 100644 --- a/src/config.ts +++ b/src/config.ts @@ -51,7 +51,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false; export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive'; export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; -export const LANG_CACHE_NAME = 'tt-lang-packs-v38'; +export const LANG_CACHE_NAME = 'tt-lang-packs-v39'; export const ASSET_CACHE_NAME = 'tt-assets'; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 7ff916084..7c52b4bda 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -511,7 +511,63 @@ addActionHandler('closePremiumGiftingModal', (global, actions, payload): ActionR }, tabId); }); -addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promise => { +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('openStarGiftInfoModal', (global, actions, payload): ActionReturnType => { + const { + toUserId, + stars, + date, + tabId = getCurrentTabId(), + } = payload || {}; + + if (!stars || !toUserId || !date) { + return; + } + + global = getGlobal(); + + global = updateTabState(global, { + starGiftInfoModal: { + toUserId, + stars, + date, + isOpen: true, + }, + }, tabId); + setGlobal(global); +}); + +addActionHandler('closeStarGiftInfoModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + starGiftInfoModal: undefined, + }, tabId); +}); + +addActionHandler('openPremiumGiftModal', async (global, actions, payload): Promise => { const { forUserIds, tabId = getCurrentTabId(), } = payload || {}; @@ -524,7 +580,7 @@ addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promi const gifts = await callApi('getPremiumGiftCodeOptions', {}); global = updateTabState(global, { - giftPremiumModal: { + giftModal: { isOpen: true, forUserIds, gifts, @@ -533,10 +589,35 @@ addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promi setGlobal(global); }); -addActionHandler('closeGiftPremiumModal', (global, actions, payload): ActionReturnType => { +addActionHandler('closePremiumGiftModal', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; global = updateTabState(global, { - giftPremiumModal: { isOpen: false }, + giftModal: { isOpen: false }, + }, tabId); + setGlobal(global); +}); + +addActionHandler('openStarsGiftModal', async (global, actions, payload): Promise => { + const { + forUserId, tabId = getCurrentTabId(), + } = payload || {}; + + const starsGiftOptions = await callApi('getStarsGiftOptions', {}); + + global = updateTabState(global, { + starsGiftModal: { + isOpen: true, + forUserId, + starsGiftOptions, + }, + }, tabId); + setGlobal(global); +}); + +addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + global = updateTabState(global, { + starsGiftModal: { isOpen: false }, }, tabId); setGlobal(global); }); diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index 55f09bd31..388a565e5 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -32,12 +32,12 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { if (!inputInvoice.userIds) { return; } - const giftModalState = selectTabState(global, tabId).giftPremiumModal; + const giftModalState = selectTabState(global, tabId).giftModal; if (giftModalState && giftModalState.isOpen && areDeepEqual(inputInvoice.userIds, giftModalState.forUserIds)) { global = updateTabState(global, { - giftPremiumModal: { + giftModal: { ...giftModalState, isCompleted: true, }, @@ -46,6 +46,24 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { } } + 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); + global = closeInvoice(global, tabId); + } + } + setGlobal(global); }); diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index 6218b4cb9..e8b48e053 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -12,6 +12,26 @@ export function getRequestInputInvoice( ): ApiRequestInputInvoice | undefined { if (inputInvoice.type === 'slug') return inputInvoice; + if (inputInvoice.type === 'starsgift') { + const { + userId, stars, amount, currency, + } = inputInvoice; + const user = selectUser(global, userId!); + + if (!user) return undefined; + + return { + type: 'stars', + purpose: { + type: 'starsgift', + user, + stars, + amount, + currency, + }, + }; + } + if (inputInvoice.type === 'stars') { const { stars, amount, currency, diff --git a/src/global/types.ts b/src/global/types.ts index baaaa2471..fa51cf09b 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -61,7 +61,7 @@ import type { ApiSendMessageAction, ApiSession, ApiSessionData, - ApiSponsoredMessage, + ApiSponsoredMessage, ApiStarsGiftOption, ApiStarsTransaction, ApiStarTopupOption, ApiStealthMode, @@ -709,11 +709,30 @@ export type TabState = { isOpen?: boolean; }; - giftPremiumModal?: { + starsGiftingModal?: { + isOpen?: boolean; + }; + + starGiftInfoModal?: { + isOpen?: boolean; + toUserId: string; + date: number; + stars: number; + }; + + starsGiftModal?: { + isCompleted?: boolean; + isOpen?: boolean; + forUserId?: string; + starsGiftOptions?: ApiStarsGiftOption[]; + }; + + giftModal?: { isCompleted?: boolean; isOpen?: boolean; forUserIds?: string[]; gifts?: ApiPremiumGiftCodeOption[]; + starsGiftOptions?: ApiStarsGiftOption[]; }; limitReachedModal?: { @@ -3162,6 +3181,9 @@ export interface ActionPayloads { openPremiumGiftingModal: WithTabId | undefined; closePremiumGiftingModal: WithTabId | undefined; + openStarsGiftingModal: WithTabId | undefined; + closeStarsGiftingModal: WithTabId | undefined; + openDeleteMessageModal: ({ message?: ApiMessage; isSchedule?: boolean; @@ -3179,12 +3201,26 @@ export interface ActionPayloads { loadDefaultTopicIcons: undefined; loadPremiumStickers: undefined; - openGiftPremiumModal: ({ + openPremiumGiftModal: ({ chatId?: string; forMultipleUsers?: boolean; forUserIds?: string[]; + isStarsGifting?: boolean; } & WithTabId) | undefined; - closeGiftPremiumModal: WithTabId | undefined; + closePremiumGiftModal: WithTabId | undefined; + + openStarsGiftModal: ({ + chatId?: string; + forUserId?: string; + } & WithTabId) | undefined; + closeStarsGiftModal: WithTabId | undefined; + + openStarGiftInfoModal: ({ + toUserId?: string; + stars?: number; + date?: number; + } & WithTabId) | undefined; + closeStarGiftInfoModal: WithTabId | undefined; setEmojiStatus: { emojiStatus: ApiSticker; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 5428b1a0e..f59624cf5 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1514,7 +1514,16 @@ export interface LangPair { 'ReplyInPrivateMessage': undefined; 'AriaSearchOlderResult': undefined; 'AriaSearchNewerResult': undefined; - + 'CreditsBoxHistoryEntryGiftOutAbout': { + 'user': string | number; + 'link': string | number; + }; + 'CreditsBoxOutAbout': { + 'link': string | number; + }; + 'GiftStarsOutgoing': { + 'user': string | number; + }; } export type LangKey = keyof LangPair; diff --git a/src/util/localization/index.ts b/src/util/localization/index.ts index e2a533d50..c5c5f062d 100644 --- a/src/util/localization/index.ts +++ b/src/util/localization/index.ts @@ -91,6 +91,7 @@ async function loadFallbackPack() { if (!language) { updateLanguage(fallbackData.language); } else { + translationFn = createTranslationFn(); scheduleCallbacks(); } }