From 46c85ebb88eeab918a763dd0c5088593cafc7ca8 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:22:19 +0200 Subject: [PATCH] Layer 181: Telegram Stars, Collapsible quotes, Fact Check (#4637) --- src/api/gramjs/apiBuilders/common.ts | 9 + src/api/gramjs/apiBuilders/messages.ts | 21 +- src/api/gramjs/apiBuilders/payments.ts | 118 ++++++- src/api/gramjs/gramjsBuilders/index.ts | 16 + src/api/gramjs/helpers.ts | 9 +- src/api/gramjs/methods/bots.ts | 8 +- src/api/gramjs/methods/index.ts | 5 +- src/api/gramjs/methods/messages.ts | 20 ++ src/api/gramjs/methods/payments.ts | 98 +++++- src/api/gramjs/updates/updater.ts | 12 +- src/api/types/messages.ts | 36 +- src/api/types/payments.ts | 82 ++++- src/api/types/updates.ts | 10 +- .../font-icons/{premium.svg => star.svg} | 0 src/assets/icons/StarLogo.svg | 1 + src/assets/stars-bg.png | Bin 0 -> 3913 bytes src/bundles/extra.ts | 2 + src/components/common/AboutAdsModal.tsx | 2 +- src/components/common/Audio.tsx | 2 +- src/components/common/Avatar.scss | 4 + src/components/common/Avatar.tsx | 9 +- src/components/common/Blockquote.module.scss | 31 ++ src/components/common/Blockquote.tsx | 65 ++++ src/components/common/Composer.tsx | 2 +- src/components/common/CountryPickerModal.tsx | 2 +- src/components/common/CustomEmojiPicker.tsx | 2 +- src/components/common/FullNameTitle.tsx | 4 +- src/components/common/GroupChatInfo.tsx | 2 +- src/components/common/LinkField.tsx | 2 +- .../common/MessageOutgoingStatus.tsx | 2 +- src/components/common/MessageText.tsx | 3 + .../common/PeerColorWrapper.module.scss | 19 ++ src/components/common/Picker.tsx | 4 +- src/components/common/PickerSelectedItem.tsx | 2 +- src/components/common/PremiumIcon.scss | 23 -- src/components/common/PremiumIcon.tsx | 56 --- src/components/common/PremiumProgress.tsx | 2 +- .../common/PrivacySettingsNoticeModal.tsx | 2 +- src/components/common/PrivateChatInfo.tsx | 4 +- src/components/common/ProfileInfo.scss | 2 +- src/components/common/ProfilePhoto.tsx | 2 +- src/components/common/SafeLink.tsx | 12 +- .../common/embedded/EmbeddedMessage.tsx | 2 +- .../common/embedded/EmbeddedStory.tsx | 2 +- .../common/embedded/EmbeddedStoryForward.tsx | 2 +- .../common/helpers/renderTextWithEntities.tsx | 14 +- src/components/common/{ => icons}/Icon.tsx | 6 +- .../common/icons/StarIcon.module.scss | 28 ++ src/components/common/icons/StarIcon.tsx | 139 ++++++++ .../common/profile/BusinessHours.tsx | 2 +- src/components/common/spoiler/Spoiler.tsx | 8 +- src/components/left/main/Chat.scss | 2 +- src/components/left/main/LeftMainHeader.scss | 2 +- src/components/left/main/StatusButton.tsx | 4 +- .../left/settings/PremiumStatusItem.tsx | 4 +- .../left/settings/PrivacyLockedOption.tsx | 2 +- src/components/left/settings/Settings.scss | 2 +- .../left/settings/SettingsExperimental.tsx | 2 +- src/components/left/settings/SettingsMain.tsx | 35 +- .../left/settings/SettingsPrivacy.tsx | 6 +- .../left/settings/SettingsPrivacyLastSeen.tsx | 4 +- .../main/AppendEntityPickerModal.tsx | 2 +- src/components/main/ConfettiContainer.tsx | 42 ++- src/components/main/Main.tsx | 2 + .../main/premium/GiftPremiumModal.tsx | 3 +- src/components/main/premium/GiveawayModal.tsx | 2 +- .../main/premium/GiveawayTypeOption.tsx | 2 +- .../main/premium/PremiumGiftingModal.tsx | 2 +- .../main/premium/PremiumMainModal.tsx | 1 + .../common/PremiumLimitReachedModal.tsx | 2 +- src/components/middle/ActionMessage.tsx | 17 +- src/components/middle/MessageList.tsx | 14 +- .../middle/PremiumRequiredMessage.tsx | 2 +- src/components/middle/composer/AttachMenu.tsx | 2 +- .../composer/ComposerEmbeddedMessage.tsx | 2 +- .../middle/message/Contact.module.scss | 15 - .../middle/message/FactCheck.module.scss | 52 +++ src/components/middle/message/FactCheck.tsx | 89 +++++ src/components/middle/message/Invoice.scss | 10 +- src/components/middle/message/Invoice.tsx | 9 +- src/components/middle/message/MentionLink.tsx | 8 +- src/components/middle/message/Message.tsx | 20 +- src/components/middle/message/RoundVideo.tsx | 2 +- .../middle/message/SimilarChannels.tsx | 2 +- .../middle/message/SponsoredMessage.tsx | 2 +- .../middle/message/_message-content.scss | 8 +- .../message/helpers/buildContentClassName.ts | 5 +- .../ReactionSelectorCustomReaction.tsx | 2 +- .../reactions/ReactionSelectorReaction.tsx | 2 +- src/components/modals/ModalContainer.tsx | 6 + src/components/modals/boost/BoostModal.tsx | 2 +- .../collectible/CollectibleInfoModal.tsx | 2 +- .../modals/common/TableInfoModal.module.scss | 37 ++ .../modals/common/TableInfoModal.tsx | 100 ++++++ .../modals/giftcode/GiftCodeModal.module.scss | 30 -- .../modals/giftcode/GiftCodeModal.tsx | 135 +++----- .../modals/reportAd/ReportAdModal.tsx | 2 +- src/components/modals/stars/BalanceBlock.tsx | 31 ++ .../modals/stars/StarsBalanceModal.async.tsx | 18 + .../stars/StarsBalanceModal.module.scss | 239 +++++++++++++ .../modals/stars/StarsBalanceModal.tsx | 251 ++++++++++++++ .../modals/stars/StarsPaymentModal.async.tsx | 18 + .../modals/stars/StarsPaymentModal.tsx | 123 +++++++ .../stars/StarsTransactionItem.module.scss | 54 +++ .../modals/stars/StarsTransactionItem.tsx | 102 ++++++ src/components/modals/webApp/WebAppModal.tsx | 5 +- src/components/payment/Checkout.module.scss | 1 + src/components/payment/Checkout.tsx | 5 +- src/components/payment/PaymentModal.scss | 4 + src/components/payment/PaymentModal.tsx | 40 ++- .../payment/ReceiptModal.module.scss | 56 +++ src/components/payment/ReceiptModal.tsx | 206 ++++++++---- src/components/right/RightHeader.tsx | 2 +- src/components/right/management/ManageBot.tsx | 2 +- .../right/management/ManageGroupMembers.tsx | 2 +- .../right/statistics/BoostStatistics.tsx | 3 +- .../statistics/StatisticsRecentMessage.tsx | 2 +- .../statistics/StatisticsRecentPostMeta.tsx | 2 +- src/components/story/MediaStory.tsx | 2 +- src/components/story/Story.tsx | 2 +- src/components/story/StoryCaption.tsx | 5 +- src/components/story/StoryFooter.tsx | 2 +- src/components/ui/Button.scss | 1 - src/components/ui/ListItem.tsx | 2 +- src/components/ui/Modal.scss | 6 + src/components/ui/Modal.tsx | 13 +- src/components/ui/ProgressSpinner.tsx | 2 +- src/components/ui/TabList.tsx | 6 +- src/config.ts | 2 +- src/global/actions/api/bots.ts | 2 +- src/global/actions/api/chats.ts | 1 + src/global/actions/api/messages.ts | 23 ++ src/global/actions/api/payments.ts | 184 ++++++++-- src/global/actions/apiUpdaters/payments.ts | 68 ++-- src/global/actions/ui/payments.ts | 37 +- src/global/helpers/payments.ts | 69 +++- src/global/reducers/payments.ts | 89 ++++- src/global/types.ts | 59 +++- src/hooks/element/useCollapsibleLines.ts | 89 +++++ src/hooks/useFocusAfterAnimation.tsx | 6 +- src/lib/gramjs/client/TelegramClient.js | 17 +- src/lib/gramjs/client/auth.ts | 6 +- src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 318 +++++++++++++++++- src/lib/gramjs/tl/apiTl.js | 40 ++- src/lib/gramjs/tl/static/api.json | 5 + src/lib/gramjs/tl/static/api.tl | 56 ++- src/styles/_mixins.scss | 12 +- src/styles/_variables.scss | 1 + src/styles/icons.scss | 94 +++--- src/styles/icons.woff | Bin 30220 -> 30200 bytes src/styles/icons.woff2 | Bin 25228 -> 25252 bytes src/styles/index.scss | 1 + src/styles/themes.json | 1 + src/types/icons/font.ts | 2 +- src/types/index.ts | 5 +- .../element/calcTextLineHeightAndCount.ts | 10 + src/util/formatCurrency.ts | 8 +- src/util/objects/customPeer.ts | 8 +- 159 files changed, 3368 insertions(+), 662 deletions(-) rename src/assets/font-icons/{premium.svg => star.svg} (100%) create mode 100644 src/assets/icons/StarLogo.svg create mode 100644 src/assets/stars-bg.png create mode 100644 src/components/common/Blockquote.module.scss create mode 100644 src/components/common/Blockquote.tsx delete mode 100644 src/components/common/PremiumIcon.scss delete mode 100644 src/components/common/PremiumIcon.tsx rename src/components/common/{ => icons}/Icon.tsx (74%) create mode 100644 src/components/common/icons/StarIcon.module.scss create mode 100644 src/components/common/icons/StarIcon.tsx create mode 100644 src/components/middle/message/FactCheck.module.scss create mode 100644 src/components/middle/message/FactCheck.tsx create mode 100644 src/components/modals/common/TableInfoModal.module.scss create mode 100644 src/components/modals/common/TableInfoModal.tsx create mode 100644 src/components/modals/stars/BalanceBlock.tsx create mode 100644 src/components/modals/stars/StarsBalanceModal.async.tsx create mode 100644 src/components/modals/stars/StarsBalanceModal.module.scss create mode 100644 src/components/modals/stars/StarsBalanceModal.tsx create mode 100644 src/components/modals/stars/StarsPaymentModal.async.tsx create mode 100644 src/components/modals/stars/StarsPaymentModal.tsx create mode 100644 src/components/modals/stars/StarsTransactionItem.module.scss create mode 100644 src/components/modals/stars/StarsTransactionItem.tsx create mode 100644 src/components/payment/ReceiptModal.module.scss create mode 100644 src/hooks/element/useCollapsibleLines.ts create mode 100644 src/util/element/calcTextLineHeightAndCount.ts diff --git a/src/api/gramjs/apiBuilders/common.ts b/src/api/gramjs/apiBuilders/common.ts index e30e39fe0..793f04a7d 100644 --- a/src/api/gramjs/apiBuilders/common.ts +++ b/src/api/gramjs/apiBuilders/common.ts @@ -260,6 +260,15 @@ export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMess }; } + if (entity instanceof GramJs.MessageEntityBlockquote) { + return { + type: ApiMessageEntityTypes.Blockquote, + canCollapse: entity.collapsed, + offset, + length, + }; + } + return { type: type as `${ApiMessageEntityDefault['type']}`, offset, diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index dfdb68dd8..161e19feb 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -6,6 +6,7 @@ import type { ApiAttachment, ApiChat, ApiContact, + ApiFactCheck, ApiGroupCall, ApiInputMessageReplyInfo, ApiInputReplyInfo, @@ -52,6 +53,7 @@ import { } from '../helpers'; import { buildApiCallDiscardReason } from './calls'; import { + buildApiFormattedText, buildApiPhoto, } from './common'; import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; @@ -149,12 +151,7 @@ export function buildApiMessageFromNotification( export type UniversalMessage = ( Pick - & Pick, ( - 'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' | - 'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' | - 'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions' | 'forwards' | 'silent' | 'pinned' | - 'savedPeerId' | 'fromBoostsApplied' | 'quickReplyShortcutId' | 'viaBusinessBotId' - )> + & Partial ); export function buildApiMessageWithChatId( @@ -192,6 +189,7 @@ export function buildApiMessageWithChatId( const emojiOnlyCount = getEmojiOnlyCountForMessage(content, groupedId); const hasComments = mtpMessage.replies?.comments; const senderBoosts = mtpMessage.fromBoostsApplied; + const factCheck = mtpMessage.factcheck && buildApiFactCheck(mtpMessage.factcheck); const savedPeerId = mtpMessage.savedPeerId && getApiChatIdFromMtpPeer(mtpMessage.savedPeerId); @@ -234,6 +232,7 @@ export function buildApiMessageWithChatId( savedPeerId, senderBoosts, viaBusinessBotId: mtpMessage.viaBusinessBotId?.toString(), + factCheck, }); } @@ -319,6 +318,15 @@ function buildApiReplyInfo(replyHeader: GramJs.TypeMessageReplyHeader): ApiReply return undefined; } +export function buildApiFactCheck(factCheck: GramJs.FactCheck): ApiFactCheck { + return { + shouldFetch: factCheck.needCheck, + hash: factCheck.hash.toString(), + text: factCheck.text && buildApiFormattedText(factCheck.text), + countryCode: factCheck.country, + }; +} + function buildAction( action: GramJs.TypeMessageAction, senderId: string | undefined, @@ -450,6 +458,7 @@ function buildAction( amount = Number(action.totalAmount); currency = action.currency; text = 'PaymentSuccessfullyPaid'; + type = 'receipt'; if (targetPeerId) { targetUserIds.push(targetPeerId); } diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 03dd48f27..02d9d862b 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -9,8 +9,12 @@ import type { ApiInvoice, ApiLabeledPrice, ApiMyBoost, ApiPaymentCredentials, ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumGiftCodeOption, ApiPremiumPromo, ApiPremiumSubscriptionOption, ApiReceipt, + ApiStarsTransaction, + ApiStarsTransactionPeer, + ApiStarTopupOption, } from '../../types'; +import { addWebDocumentToLocalDb } from '../helpers'; import { buildApiMessageEntity } from './common'; import { omitVirtualClassFields } from './helpers'; import { buildApiDocument, buildApiWebDocument } from './messageContent'; @@ -37,7 +41,35 @@ export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | }); } -export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiReceipt { +export function buildApiReceipt(receipt: GramJs.payments.TypePaymentReceipt): ApiReceipt { + const { photo } = receipt; + + if (photo) { + addWebDocumentToLocalDb(photo); + } + + if (receipt instanceof GramJs.payments.PaymentReceiptStars) { + const { + botId, currency, date, description: text, title, totalAmount, transactionId, + } = receipt; + + if (photo) { + addWebDocumentToLocalDb(photo); + } + + return { + type: 'stars', + currency, + botId: buildApiPeerId(botId, 'user'), + date, + text, + title, + totalAmount: -totalAmount.toJSNumber(), + transactionId, + photo: photo && buildApiWebDocument(photo), + }; + } + const { invoice, info, @@ -46,6 +78,8 @@ export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiRec totalAmount, credentialsTitle, tipAmount, + title, + description: text, } = receipt; const { shippingAddress, phone, name } = (info || {}); @@ -70,6 +104,7 @@ export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiRec } return { + type: 'regular', currency, prices: mappedPrices, info: { shippingAddress, phone, name }, @@ -78,10 +113,23 @@ export function buildApiReceipt(receipt: GramJs.payments.PaymentReceipt): ApiRec shippingPrices, shippingMethod, tipAmount: tipAmount ? tipAmount.toJSNumber() : 0, + title, + text, + photo: photo && buildApiWebDocument(photo), }; } -export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPaymentForm { +export function buildApiPaymentForm(form: GramJs.payments.TypePaymentForm): ApiPaymentForm { + if (form instanceof GramJs.payments.PaymentFormStars) { + const { botId, formId } = form; + + return { + type: 'stars', + botId: buildApiPeerId(botId, 'user'), + formId: String(formId), + }; + } + const { formId, canSaveCredentials, @@ -93,6 +141,7 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme invoice, savedCredentials, url, + botId, } = form; const { @@ -121,7 +170,9 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme const nativeData = nativeParams ? JSON.parse(nativeParams.data) : {}; return { + type: 'regular', url, + botId: buildApiPeerId(botId, 'user'), canSaveCredentials, isPasswordMissing, formId: String(formId), @@ -146,12 +197,13 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme needZip: Boolean(nativeData?.need_zip), publishableKey: nativeData?.publishable_key, publicToken: nativeData?.public_token, + tokenizeUrl: nativeData?.tokenize_url, }, - ...(savedCredentials && { savedCredentials: buildApiPaymentCredentials(savedCredentials) }), + savedCredentials: savedCredentials && buildApiPaymentCredentials(savedCredentials), }; } -export function buildApiInvoiceFromForm(form: GramJs.payments.PaymentForm): ApiInvoice { +export function buildApiInvoiceFromForm(form: GramJs.payments.TypePaymentForm): ApiInvoice { const { invoice, description: text, title, photo, } = form; @@ -328,3 +380,61 @@ export function buildApiPremiumGiftCodeOption(option: GramJs.PremiumGiftCodeOpti users, }; } + +export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPeer): ApiStarsTransactionPeer { + if (peer instanceof GramJs.StarsTransactionPeerAppStore) { + return { type: 'appStore' }; + } + + if (peer instanceof GramJs.StarsTransactionPeerPlayMarket) { + return { type: 'playMarket' }; + } + + if (peer instanceof GramJs.StarsTransactionPeerPremiumBot) { + return { type: 'premiumBot' }; + } + + if (peer instanceof GramJs.StarsTransactionPeerFragment) { + return { type: 'fragment' }; + } + + if (peer instanceof GramJs.StarsTransactionPeer) { + return { type: 'peer', id: getApiChatIdFromMtpPeer(peer.peer) }; + } + + return { type: 'unsupported' }; +} + +export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction { + const { + date, id, peer, stars, description, photo, title, refund, + } = transaction; + + if (photo) { + addWebDocumentToLocalDb(photo); + } + + return { + id, + date, + peer: buildApiStarsTransactionPeer(peer), + stars: stars.toJSNumber(), + title, + description, + photo: photo && buildApiWebDocument(photo), + isRefund: refund, + }; +} + +export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): ApiStarTopupOption { + const { + amount, currency, stars, extended, + } = option; + + return { + amount: amount.toJSNumber(), + currency, + stars: stars.toJSNumber(), + isExtended: extended, + }; +} diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 9b7872592..3ff36226e 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -22,6 +22,7 @@ import type { ApiReportReason, ApiRequestInputInvoice, ApiSendMessageAction, + ApiStarTopupOption, ApiSticker, ApiStory, ApiStorySkipped, @@ -604,6 +605,15 @@ function buildPremiumGiftCodeOption(optionData: ApiPremiumGiftCodeOption) { }); } +function buildInputStarsTopupOption(option: ApiStarTopupOption) { + return new GramJs.StarsTopupOption({ + stars: BigInt(option.stars), + amount: BigInt(option.amount), + currency: option.currency, + extended: option.isExtended, + }); +} + export function buildInputInvoice(invoice: ApiRequestInputInvoice) { switch (invoice.type) { case 'message': { @@ -619,6 +629,12 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) { }); } + case 'stars': { + return new GramJs.InputInvoiceStars({ + option: buildInputStarsTopupOption(invoice.option), + }); + } + case 'giveaway': default: { const purpose = buildInputStorePaymentPurpose(invoice.purpose); diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index a1a1b931f..50d9dcb4a 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -83,9 +83,8 @@ function addMediaToLocalDb(media: GramJs.TypeMessageMedia) { addPhotoToLocalDb(media.game.photo); } - if (media instanceof GramJs.MessageMediaInvoice - && media.photo) { - localDb.webDocuments[String(media.photo.url)] = media.photo; + if (media instanceof GramJs.MessageMediaInvoice && media.photo) { + addWebDocumentToLocalDb(media.photo); } } @@ -154,6 +153,10 @@ export function addEntitiesToLocalDb(entities: (GramJs.TypeUser | GramJs.TypeCha }); } +export function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) { + localDb.webDocuments[webDocument.url] = webDocument; +} + export function swapLocalInvoiceMedia( chatId: string, messageId: number, extendedMedia: GramJs.TypeMessageExtendedMedia, ) { diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 5adb19501..e1d2fcc44 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -33,7 +33,9 @@ import { buildInputThemeParams, generateRandomBigInt, } from '../gramjsBuilders'; -import { addEntitiesToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; +import { + addEntitiesToLocalDb, addUserToLocalDb, addWebDocumentToLocalDb, deserializeBytes, +} from '../helpers'; import localDb from '../localDb'; import { invokeRequest } from './client'; @@ -561,10 +563,6 @@ function addPhotoToLocalDb(photo: GramJs.Photo) { localDb.photos[String(photo.id)] = photo; } -function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) { - localDb.webDocuments[webDocument.url] = webDocument; -} - export function setBotInfo({ bot, langCode, diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 42bec0586..5de53a808 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -33,7 +33,7 @@ export { reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage, - fetchOutboxReadDate, exportMessageLink, fetchQuickReplies, sendQuickReply, + fetchOutboxReadDate, exportMessageLink, fetchQuickReplies, sendQuickReply, fetchFactChecks, deleteSavedHistory, } from './messages'; @@ -100,7 +100,8 @@ export * from './stories'; export { validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword, applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode, - getPremiumGiftCodeOptions, launchPrepaidGiveaway, + getPremiumGiftCodeOptions, launchPrepaidGiveaway, fetchStarsStatus, fetchStarsTopupOptions, fetchStarsTransactions, + sendStarPaymentForm, } from './payments'; export * from './fragment'; diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index c237193cf..27e4a2efa 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -49,6 +49,7 @@ import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/ch import { buildApiFormattedText } from '../apiBuilders/common'; import { buildMessageMediaContent, buildMessageTextContent, buildWebPage } from '../apiBuilders/messageContent'; import { + buildApiFactCheck, buildApiMessage, buildApiQuickReply, buildApiSponsoredMessage, @@ -1047,6 +1048,25 @@ export async function fetchMessageViews({ }; } +export async function fetchFactChecks({ + chat, ids, +}: { + chat: ApiChat; + ids: number[]; +}) { + const chunks = split(ids, API_GENERAL_ID_LIMIT); + const results = await Promise.all(chunks.map((chunkIds) => ( + invokeRequest(new GramJs.messages.GetFactCheck({ + peer: buildInputPeer(chat.id, chat.accessHash), + msgId: chunkIds, + })) + ))); + + if (!results || results.some((result) => !result)) return undefined; + + return results.flatMap((result) => result!).map(buildApiFactCheck); +} + export async function fetchDiscussionMessage({ chat, messageId, }: { diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index ceba4f0bb..1cb6c8f3f 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -6,6 +6,7 @@ import type { OnApiUpdate, } from '../../types'; +import { DEBUG } from '../../../config'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiBoost, @@ -18,6 +19,8 @@ import { buildApiPremiumGiftCodeOption, buildApiPremiumPromo, buildApiReceipt, + buildApiStarsTransaction, + buildApiStarTopupOption, buildShippingOptions, } from '../apiBuilders/payments'; import { buildApiUser } from '../apiBuilders/users'; @@ -26,6 +29,7 @@ import { } from '../gramjsBuilders'; import { addEntitiesToLocalDb, + addWebDocumentToLocalDb, deserializeBytes, serializeBytes, } from '../helpers'; @@ -124,6 +128,34 @@ export async function sendPaymentForm({ return Boolean(result); } +export async function sendStarPaymentForm({ + formId, + inputInvoice, +}: { + formId: string; + inputInvoice: ApiRequestInputInvoice; +}) { + const result = await invokeRequest(new GramJs.payments.SendStarsForm({ + formId: BigInt(formId), + invoice: buildInputInvoice(inputInvoice), + })); + + if (!result) return false; + + if (result instanceof GramJs.payments.PaymentVerificationNeeded) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.warn('Unexpected PaymentVerificationNeeded in sendStarsForm'); + } + + return undefined; + } else { + handleGramJsUpdate(result.updates); + } + + return Boolean(result); +} + export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) { const result = await invokeRequest(new GramJs.payments.GetPaymentForm({ invoice: buildInputInvoice(inputInvoice), @@ -134,7 +166,7 @@ export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) { } if (result.photo) { - localDb.webDocuments[result.photo.url] = result.photo; + addWebDocumentToLocalDb(result.photo); } addEntitiesToLocalDb(result.users); @@ -143,7 +175,6 @@ export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice) { form: buildApiPaymentForm(result), invoice: buildApiInvoiceFromForm(result), users: result.users.map(buildApiUser).filter(Boolean), - botId: result.botId.toString(), }; } @@ -387,3 +418,66 @@ export function launchPrepaidGiveaway({ shouldReturnTrue: true, }); } + +export async function fetchStarsStatus() { + const result = await invokeRequest(new GramJs.payments.GetStarsStatus({ + peer: new GramJs.InputPeerSelf(), + })); + + if (!result) { + return undefined; + } + + const users = result.users.map(buildApiUser).filter(Boolean); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); + + return { + users, + chats, + nextOffset: result.nextOffset, + history: result.history.map(buildApiStarsTransaction), + balance: result.balance.toJSNumber(), + }; +} + +export async function fetchStarsTransactions({ + offset, + isInbound, + isOutbound, +}: { + offset?: string; + isInbound?: true; + isOutbound?: true; +}) { + const result = await invokeRequest(new GramJs.payments.GetStarsTransactions({ + peer: new GramJs.InputPeerSelf(), + offset, + inbound: isInbound, + outbound: isOutbound, + })); + + if (!result) { + return undefined; + } + + const users = result.users.map(buildApiUser).filter(Boolean); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); + + return { + users, + chats, + nextOffset: result.nextOffset, + history: result.history.map(buildApiStarsTransaction), + balance: result.balance.toJSNumber(), + }; +} + +export async function fetchStarsTopupOptions() { + const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions()); + + if (!result) { + return undefined; + } + + return result.map(buildApiStarTopupOption); +} diff --git a/src/api/gramjs/updates/updater.ts b/src/api/gramjs/updates/updater.ts index b852c2400..e56826805 100644 --- a/src/api/gramjs/updates/updater.ts +++ b/src/api/gramjs/updates/updater.ts @@ -244,12 +244,7 @@ export function updater(update: Update) { if (update.message instanceof GramJs.MessageService) { const { action } = update.message; - if (action instanceof GramJs.MessageActionPaymentSent) { - onUpdate({ - '@type': 'updatePaymentStateCompleted', - slug: action.invoiceSlug, - }); - } else if (action instanceof GramJs.MessageActionChatEditTitle) { + if (action instanceof GramJs.MessageActionChatEditTitle) { onUpdate({ '@type': 'updateChat', id: message.chatId, @@ -1194,6 +1189,11 @@ export function updater(update: Update) { chatId: buildApiPeerId(update.channelId, 'channel'), isEnabled: update.enabled ? true : undefined, }); + } else if (update instanceof GramJs.UpdateStarsBalance) { + onUpdate({ + '@type': 'updateStarsBalance', + balance: update.balance.toJSNumber(), + }); } else if (update instanceof LocalUpdatePremiumFloodWait) { onUpdate({ '@type': 'updatePremiumFloodWait', diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index a21562053..7687f7a7e 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -2,7 +2,7 @@ import type { ThreadId } from '../../types'; import type { ApiWebDocument } from './bots'; import type { ApiGroupCall, PhoneCallAction } from './calls'; import type { ApiChat, ApiPeerColor } from './chats'; -import type { ApiInputStorePaymentPurpose, ApiPremiumGiftCodeOption } from './payments'; +import type { ApiInputStorePaymentPurpose, ApiPremiumGiftCodeOption, ApiStarTopupOption } from './payments'; import type { ApiMessageStoryData, ApiWebPageStickerData, ApiWebPageStoryData } from './stories'; export interface ApiDimensions { @@ -208,8 +208,13 @@ export type ApiInputInvoiceGiftCode = { option: ApiPremiumGiftCodeOption; }; +export type ApiInputInvoiceStars = { + type: 'stars'; + option: ApiStarTopupOption; +}; + export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway -| ApiInputInvoiceGiftCode; +| ApiInputInvoiceGiftCode | ApiInputInvoiceStars; /* Used for Invoice request */ export type ApiRequestInputInvoiceMessage = { @@ -229,8 +234,13 @@ export type ApiRequestInputInvoiceGiveaway = { option: ApiPremiumGiftCodeOption; }; +export type ApiRequestInputInvoiceStars = { + type: 'stars'; + option: ApiStarTopupOption; +}; + export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug -| ApiRequestInputInvoiceGiveaway; +| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars; export interface ApiInvoice { text: string; @@ -345,6 +355,7 @@ export interface ApiAction { | 'suggestProfilePhoto' | 'joinedChannel' | 'chatBoost' + | 'receipt' | 'other'; photo?: ApiPhoto; amount?: number; @@ -445,7 +456,7 @@ export type ApiMessageEntityDefault = { type: Exclude< `${ApiMessageEntityTypes}`, `${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` | - `${ApiMessageEntityTypes.CustomEmoji}` + `${ApiMessageEntityTypes.CustomEmoji}` | `${ApiMessageEntityTypes.Blockquote}` >; offset: number; length: number; @@ -472,6 +483,13 @@ export type ApiMessageEntityMentionName = { userId: string; }; +export type ApiMessageEntityBlockquote = { + type: ApiMessageEntityTypes.Blockquote; + offset: number; + length: number; + canCollapse?: boolean; +}; + export type ApiMessageEntityCustomEmoji = { type: ApiMessageEntityTypes.CustomEmoji; offset: number; @@ -480,7 +498,7 @@ export type ApiMessageEntityCustomEmoji = { }; export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl | -ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji; +ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote; export enum ApiMessageEntityTypes { Bold = 'MessageEntityBold', @@ -587,6 +605,7 @@ export interface ApiMessage { readDate?: number; savedPeerId?: string; senderBoosts?: number; + factCheck?: ApiFactCheck; } export interface ApiReactions { @@ -835,6 +854,13 @@ export type ApiQuickReply = { topMessageId: number; }; +export type ApiFactCheck = { + shouldFetch?: true; + hash: string; + countryCode?: string; + text?: ApiFormattedText; +}; + export type ApiSponsoredMessageReportResult = { type: 'reported' | 'hidden' | 'premiumRequired'; } | { diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index 4b7bf9637..d7716bed1 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -22,8 +22,10 @@ export interface ApiPaymentSavedInfo { shippingAddress?: ApiShippingAddress; } -export interface ApiPaymentForm { +export interface ApiPaymentFormRegular { + type: 'regular'; url: string; + botId: string; canSaveCredentials?: boolean; isPasswordMissing?: boolean; formId: string; @@ -35,12 +37,21 @@ export interface ApiPaymentForm { nativeParams: ApiPaymentFormNativeParams; } +export interface ApiPaymentFormStars { + type: 'stars'; + formId: string; + botId: string; +} + +export type ApiPaymentForm = ApiPaymentFormRegular | ApiPaymentFormStars; + export interface ApiPaymentFormNativeParams { needCardholderName?: boolean; needCountry?: boolean; needZip?: boolean; publishableKey?: string; publicToken?: string; + tokenizeUrl?: string; } export interface ApiLabeledPrice { @@ -48,7 +59,21 @@ export interface ApiLabeledPrice { amount: number; } -export interface ApiReceipt { +export interface ApiReceiptStars { + type: 'stars'; + botId?: string; + peer?: ApiStarsTransactionPeer; + date: number; + title?: string; + text?: string; + photo?: ApiWebDocument; + currency: string; + totalAmount: number; + transactionId: string; +} + +export interface ApiReceiptRegular { + type: 'regular'; photo?: ApiWebDocument; text?: string; title?: string; @@ -66,6 +91,8 @@ export interface ApiReceipt { shippingMethod?: string; } +export type ApiReceipt = ApiReceiptRegular | ApiReceiptStars; + export interface ApiPremiumPromo { videoSections: ApiPremiumSection[]; videos: ApiDocument[]; @@ -179,3 +206,54 @@ export interface ApiPrepaidGiveaway { quantity: number; date: number; } + +export interface ApiStarsTransactionPeerUnsupported { + type: 'unsupported'; +} + +export interface ApiStarsTransactionPeerAppStore { + type: 'appStore'; +} + +export interface ApiStarsTransactionPeerPlayMarket { + type: 'playMarket'; +} + +export interface ApiStarsTransactionPeerPremiumBot { + type: 'premiumBot'; +} + +export interface ApiStarsTransactionPeerFragment { + type: 'fragment'; +} + +export interface ApiStarsTransactionPeerPeer { + type: 'peer'; + id: string; +} + +export type ApiStarsTransactionPeer = +| ApiStarsTransactionPeerUnsupported +| ApiStarsTransactionPeerAppStore +| ApiStarsTransactionPeerPlayMarket +| ApiStarsTransactionPeerPremiumBot +| ApiStarsTransactionPeerFragment +| ApiStarsTransactionPeerPeer; + +export interface ApiStarsTransaction { + id: string; + peer: ApiStarsTransactionPeer; + stars: number; + isRefund?: true; + date: number; + title?: string; + description?: string; + photo?: ApiWebDocument; +} + +export interface ApiStarTopupOption { + isExtended?: true; + stars: number; + currency: string; + amount: number; +} diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 921912960..8056071f1 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -20,6 +20,7 @@ import type { } from './chats'; import type { ApiFormattedText, + ApiInputInvoice, ApiMessage, ApiMessageExtendedMediaPreview, ApiPhoto, @@ -518,7 +519,7 @@ export type ApiUpdatePaymentVerificationNeeded = { export type ApiUpdatePaymentStateCompleted = { '@type': 'updatePaymentStateCompleted'; - slug?: string; + inputInvoice: ApiInputInvoice; }; export type ApiUpdatePrivacy = { @@ -738,6 +739,11 @@ export type ApiUpdatePremiumFloodWait = { isUpload?: boolean; }; +export type ApiUpdateStarsBalance = { + '@type': 'updateStarsBalance'; + balance: number; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -768,7 +774,7 @@ export type ApiUpdate = ( ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden | ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage | - ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | + ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages ); diff --git a/src/assets/font-icons/premium.svg b/src/assets/font-icons/star.svg similarity index 100% rename from src/assets/font-icons/premium.svg rename to src/assets/font-icons/star.svg diff --git a/src/assets/icons/StarLogo.svg b/src/assets/icons/StarLogo.svg new file mode 100644 index 000000000..d2058b1c8 --- /dev/null +++ b/src/assets/icons/StarLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/stars-bg.png b/src/assets/stars-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..05cf31ba22b22e9fcd665d5df1a908b80a44a27b GIT binary patch literal 3913 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 = ({ size = 'large', peer, photo, + webPhoto, text, isSavedMessages, isSavedDialog, @@ -118,6 +122,8 @@ const Avatar: FC = ({ if (photo.isVideo && withVideo) { videoHash = `videoAvatar${photo.id}?size=u`; } + } else if (webPhoto) { + imageHash = getWebDocumentHash(webPhoto); } } @@ -230,6 +236,7 @@ const Avatar: FC = ({ isReplies && 'replies-bot-account', isPremiumGradient && 'premium-gradient-bg', isRoundedRect && 'forum', + (photo || webPhoto) && 'force-fit', ((withStory && realPeer?.hasStories) || forPremiumPromo) && 'with-story-circle', withStorySolid && realPeer?.hasStories && 'with-story-solid', withStorySolid && forceFriendStorySolid && 'close-friend', diff --git a/src/components/common/Blockquote.module.scss b/src/components/common/Blockquote.module.scss new file mode 100644 index 000000000..e988252be --- /dev/null +++ b/src/components/common/Blockquote.module.scss @@ -0,0 +1,31 @@ +@use '../../styles/mixins'; + +.root { + display: inline-block; + width: 100%; +} + +.collapsed { + @include mixins.gradient-border-bottom(1rem); +} + +.gradientContainer { + max-height: inherit; +} + +.collapseIcon { + position: absolute; + display: grid; + place-items: center; + + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + + bottom: 0; + right: 0; +} + +.clickable { + cursor: var(--custom-cursor, pointer); +} diff --git a/src/components/common/Blockquote.tsx b/src/components/common/Blockquote.tsx new file mode 100644 index 000000000..517a64ffb --- /dev/null +++ b/src/components/common/Blockquote.tsx @@ -0,0 +1,65 @@ +import React, { + type TeactNode, + useRef, +} from '../../lib/teact/teact'; + +import { ApiMessageEntityTypes } from '../../api/types'; + +import buildClassName from '../../util/buildClassName'; + +import useCollapsibleLines from '../../hooks/element/useCollapsibleLines'; +import useLastCallback from '../../hooks/useLastCallback'; + +import Icon from './icons/Icon'; + +import styles from './Blockquote.module.scss'; + +type OwnProps = { + canBeCollapsible?: boolean; + isToggleDisabled?: boolean; + children: TeactNode; +}; + +const MAX_LINES = 4; + +const Blockquote = ({ canBeCollapsible, isToggleDisabled, children }: OwnProps) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + const { + isCollapsed, isCollapsible, setIsCollapsed, + } = useCollapsibleLines(ref, MAX_LINES, undefined, !canBeCollapsible); + + const canExpand = !isToggleDisabled && isCollapsed; + + const handleExpand = useLastCallback(() => { + setIsCollapsed(false); + }); + + const handleToggle = useLastCallback(() => { + setIsCollapsed((prev) => !prev); + }); + + return ( + +
+
+ {children} +
+ {isCollapsible && ( +
+ +
+ )} +
+
+ ); +}; + +export default Blockquote; diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 95d4cbb97..973f3b5fd 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -158,7 +158,7 @@ import ResponsiveHoverButton from '../ui/ResponsiveHoverButton'; import Spinner from '../ui/Spinner'; import Avatar from './Avatar'; import DeleteMessageModal from './DeleteMessageModal.async'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji'; import './Composer.scss'; diff --git a/src/components/common/CountryPickerModal.tsx b/src/components/common/CountryPickerModal.tsx index 92a64133c..d4994ebfb 100644 --- a/src/components/common/CountryPickerModal.tsx +++ b/src/components/common/CountryPickerModal.tsx @@ -14,7 +14,7 @@ import usePrevious from '../../hooks/usePrevious'; import Button from '../ui/Button'; import Modal from '../ui/Modal'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import Picker from './Picker'; import styles from './CountryPickerModal.module.scss'; diff --git a/src/components/common/CustomEmojiPicker.tsx b/src/components/common/CustomEmojiPicker.tsx index 5448298ae..36d469f08 100644 --- a/src/components/common/CustomEmojiPicker.tsx +++ b/src/components/common/CustomEmojiPicker.tsx @@ -43,7 +43,7 @@ import { useStickerPickerObservers } from './hooks/useStickerPickerObservers'; import StickerSetCover from '../middle/composer/StickerSetCover'; import Button from '../ui/Button'; import Loading from '../ui/Loading'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import StickerButton from './StickerButton'; import StickerSet from './StickerSet'; diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index 721ae701f..e3cc51d5b 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -22,7 +22,7 @@ import useLastCallback from '../../hooks/useLastCallback'; import CustomEmoji from './CustomEmoji'; import FakeIcon from './FakeIcon'; -import PremiumIcon from './PremiumIcon'; +import StarIcon from './icons/StarIcon'; import VerifiedIcon from './VerifiedIcon'; import styles from './FullNameTitle.module.scss'; @@ -127,7 +127,7 @@ const FullNameTitle: FC = ({ onClick={onEmojiStatusClick} /> )} - {withEmojiStatus && !realPeer?.emojiStatus && isPremium && } + {withEmojiStatus && !realPeer?.emojiStatus && isPremium && } )} {iconElement} diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx index e1aca8926..d854f1e34 100644 --- a/src/components/common/GroupChatInfo.tsx +++ b/src/components/common/GroupChatInfo.tsx @@ -33,7 +33,7 @@ import Transition from '../ui/Transition'; import Avatar from './Avatar'; import DotAnimation from './DotAnimation'; import FullNameTitle from './FullNameTitle'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import TopicIcon from './TopicIcon'; import TypingStatus from './TypingStatus'; diff --git a/src/components/common/LinkField.tsx b/src/components/common/LinkField.tsx index e932ba3a1..0ad2fdcd3 100644 --- a/src/components/common/LinkField.tsx +++ b/src/components/common/LinkField.tsx @@ -12,7 +12,7 @@ import useLastCallback from '../../hooks/useLastCallback'; import Button from '../ui/Button'; import DropdownMenu from '../ui/DropdownMenu'; import MenuItem from '../ui/MenuItem'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import styles from './LinkField.module.scss'; diff --git a/src/components/common/MessageOutgoingStatus.tsx b/src/components/common/MessageOutgoingStatus.tsx index 28aa03348..17d3341fa 100644 --- a/src/components/common/MessageOutgoingStatus.tsx +++ b/src/components/common/MessageOutgoingStatus.tsx @@ -4,7 +4,7 @@ import React, { memo } from '../../lib/teact/teact'; import type { ApiMessageOutgoingStatus } from '../../api/types'; import Transition from '../ui/Transition'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import './MessageOutgoingStatus.scss'; diff --git a/src/components/common/MessageText.tsx b/src/components/common/MessageText.tsx index 89a24a335..d67743a46 100644 --- a/src/components/common/MessageText.tsx +++ b/src/components/common/MessageText.tsx @@ -29,6 +29,7 @@ interface OwnProps { inChatList?: boolean; forcePlayback?: boolean; focusedQuote?: string; + isInSelectMode?: boolean; } const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3; @@ -49,6 +50,7 @@ function MessageText({ inChatList, forcePlayback, focusedQuote, + isInSelectMode, }: OwnProps) { // eslint-disable-next-line no-null/no-null const sharedCanvasRef = useRef(null); @@ -104,6 +106,7 @@ function MessageText({ cacheBuster: textCacheBusterRef.current.toString(), forcePlayback, focusedQuote, + isInSelectMode, }), ].flat().filter(Boolean)} diff --git a/src/components/common/PeerColorWrapper.module.scss b/src/components/common/PeerColorWrapper.module.scss index 6e91c9f00..3c9fa5fb3 100644 --- a/src/components/common/PeerColorWrapper.module.scss +++ b/src/components/common/PeerColorWrapper.module.scss @@ -1,3 +1,22 @@ .root { position: relative; + overflow: hidden; + + padding-inline-start: 0.5rem; + + border-radius: 0.25rem; + + background-color: var(--accent-background-color); + color: var(--accent-color); + + &::before { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + inset-inline-start: 0; + width: 0.1875rem; + background: var(--bar-gradient, var(--accent-color)); + } } diff --git a/src/components/common/Picker.tsx b/src/components/common/Picker.tsx index 83cd6355d..db1069b48 100644 --- a/src/components/common/Picker.tsx +++ b/src/components/common/Picker.tsx @@ -4,7 +4,7 @@ import React, { } from '../../lib/teact/teact'; import type { ApiCountry } from '../../api/types'; -import type { CustomPeer, CustomPeerType } from '../../types'; +import type { CustomPeer, CustomPeerType, UniqueCustomPeer } from '../../types'; import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { isUserId } from '../../global/helpers'; @@ -28,7 +28,7 @@ import './Picker.scss'; type OwnProps = { className?: string; - categories?: CustomPeer[]; + categories?: UniqueCustomPeer[]; itemIds: string[]; selectedCategories?: CustomPeerType[]; selectedIds: string[]; diff --git a/src/components/common/PickerSelectedItem.tsx b/src/components/common/PickerSelectedItem.tsx index d42594826..df7e8c922 100644 --- a/src/components/common/PickerSelectedItem.tsx +++ b/src/components/common/PickerSelectedItem.tsx @@ -15,7 +15,7 @@ import renderText from './helpers/renderText'; import useLang from '../../hooks/useLang'; import Avatar from './Avatar'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import './PickerSelectedItem.scss'; diff --git a/src/components/common/PremiumIcon.scss b/src/components/common/PremiumIcon.scss deleted file mode 100644 index 059604f77..000000000 --- a/src/components/common/PremiumIcon.scss +++ /dev/null @@ -1,23 +0,0 @@ -.PremiumIcon { - flex-shrink: 0; - display: flex; - width: 1rem; - height: 1rem; - - &.big { - width: 1.5rem; - height: 1.5rem; - } - - --color-fill: var(--color-primary); - - & > svg { - width: 100%; - height: 100%; - } - - &.clickable { - cursor: var(--custom-cursor, pointer); - pointer-events: auto; - } -} diff --git a/src/components/common/PremiumIcon.tsx b/src/components/common/PremiumIcon.tsx deleted file mode 100644 index ec9512887..000000000 --- a/src/components/common/PremiumIcon.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { memo } from '../../lib/teact/teact'; - -import buildClassName from '../../util/buildClassName'; - -import useUniqueId from '../../hooks/useUniqueId'; - -import './PremiumIcon.scss'; - -type OwnProps = { - withGradient?: boolean; - big?: boolean; - className?: string; - onClick?: VoidFunction; -}; - -// eslint-disable-next-line max-len -const STAR_PATH = 'M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z'; - -const PremiumIcon: FC = ({ - withGradient, - big, - className, - onClick, -}) => { - const randomId = useUniqueId(); - - return ( - - {withGradient ? ( - - - - - - - - - - - ) : ( - - - - )} - - ); -}; - -export default memo(PremiumIcon); diff --git a/src/components/common/PremiumProgress.tsx b/src/components/common/PremiumProgress.tsx index 02da139e9..b375d0964 100644 --- a/src/components/common/PremiumProgress.tsx +++ b/src/components/common/PremiumProgress.tsx @@ -11,7 +11,7 @@ import buildStyle from '../../util/buildStyle'; import useLang from '../../hooks/useLang'; import useResizeObserver from '../../hooks/useResizeObserver'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import styles from './PremiumProgress.module.scss'; diff --git a/src/components/common/PrivacySettingsNoticeModal.tsx b/src/components/common/PrivacySettingsNoticeModal.tsx index da9e5ba73..e12d09c18 100644 --- a/src/components/common/PrivacySettingsNoticeModal.tsx +++ b/src/components/common/PrivacySettingsNoticeModal.tsx @@ -16,7 +16,7 @@ import Button from '../ui/Button'; import Modal, { ANIMATION_DURATION } from '../ui/Modal'; import Separator from '../ui/Separator'; import AnimatedIconWithPreview from './AnimatedIconWithPreview'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import styles from './PrivacySettingsNoticeModal.module.scss'; diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 5e06ec2e9..3b216411f 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -23,7 +23,7 @@ import RippleEffect from '../ui/RippleEffect'; import Avatar from './Avatar'; import DotAnimation from './DotAnimation'; import FullNameTitle from './FullNameTitle'; -import Icon from './Icon'; +import Icon from './icons/Icon'; import TypingStatus from './TypingStatus'; type OwnProps = { @@ -227,7 +227,7 @@ const PrivateChatInfo: FC = ({ /> )} = ({ +const SafeLink = ({ url, text, className, children, isRtl, -}) => { +}: OwnProps) => { const { openUrl } = getActions(); const content = children || text; @@ -96,4 +96,4 @@ function getUnicodeUrl(url?: string) { return undefined; } -export default memo(SafeLink); +export default SafeLink; diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index 09926cb4a..2710f3db3 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -37,7 +37,7 @@ import useThumbnail from '../../../hooks/useThumbnail'; import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation'; import ActionMessage from '../../middle/ActionMessage'; -import Icon from '../Icon'; +import Icon from '../icons/Icon'; import MediaSpoiler from '../MediaSpoiler'; import MessageSummary from '../MessageSummary'; import EmojiIconBackground from './EmojiIconBackground'; diff --git a/src/components/common/embedded/EmbeddedStory.tsx b/src/components/common/embedded/EmbeddedStory.tsx index e346e9f3f..7a3fe9b1e 100644 --- a/src/components/common/embedded/EmbeddedStory.tsx +++ b/src/components/common/embedded/EmbeddedStory.tsx @@ -20,7 +20,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; -import Icon from '../Icon'; +import Icon from '../icons/Icon'; import './EmbeddedMessage.scss'; diff --git a/src/components/common/embedded/EmbeddedStoryForward.tsx b/src/components/common/embedded/EmbeddedStoryForward.tsx index 082faa5e9..830da31cf 100644 --- a/src/components/common/embedded/EmbeddedStoryForward.tsx +++ b/src/components/common/embedded/EmbeddedStoryForward.tsx @@ -23,7 +23,7 @@ import { useFastClick } from '../../../hooks/useFastClick'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import Icon from '../Icon'; +import Icon from '../icons/Icon'; import EmojiIconBackground from './EmojiIconBackground'; import './EmbeddedMessage.scss'; diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index 2618106ac..ff2740778 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -14,6 +14,7 @@ import { buildCustomEmojiHtmlFromEntity } from '../../middle/composer/helpers/cu import renderText from './renderText'; import MentionLink from '../../middle/message/MentionLink'; +import Blockquote from '../Blockquote'; import CodeBlock from '../code/CodeBlock'; import CustomEmoji from '../CustomEmoji'; import SafeLink from '../SafeLink'; @@ -46,6 +47,7 @@ export function renderTextWithEntities({ cacheBuster, forcePlayback, focusedQuote, + isInSelectMode, }: { text: string; entities?: ApiMessageEntity[]; @@ -64,6 +66,7 @@ export function renderTextWithEntities({ cacheBuster?: string; forcePlayback?: boolean; focusedQuote?: string; + isInSelectMode?: boolean; }) { if (!entities?.length) { return renderMessagePart({ @@ -170,6 +173,7 @@ export function renderTextWithEntities({ sharedCanvasHqRef, cacheBuster, forcePlayback, + isInSelectMode, }); if (Array.isArray(newEntity)) { @@ -384,6 +388,7 @@ function processEntity({ sharedCanvasHqRef, cacheBuster, forcePlayback, + isInSelectMode, } : { entity: ApiMessageEntity; entityContent: TextPart; @@ -402,6 +407,7 @@ function processEntity({ sharedCanvasHqRef?: React.RefObject; cacheBuster?: string; forcePlayback?: boolean; + isInSelectMode?: boolean; }) { const entityText = typeof entityContent === 'string' && entityContent; const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent; @@ -451,11 +457,9 @@ function processEntity({ return {renderNestedMessagePart()}; case ApiMessageEntityTypes.Blockquote: return ( - -
- {renderNestedMessagePart()} -
-
+
+ {renderNestedMessagePart()} +
); case ApiMessageEntityTypes.BotCommand: return ( diff --git a/src/components/common/Icon.tsx b/src/components/common/icons/Icon.tsx similarity index 74% rename from src/components/common/Icon.tsx rename to src/components/common/icons/Icon.tsx index 1575f4f44..4c4625bba 100644 --- a/src/components/common/Icon.tsx +++ b/src/components/common/icons/Icon.tsx @@ -1,9 +1,9 @@ import type { AriaRole } from 'react'; -import React from '../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; -import type { IconName } from '../../types/icons'; +import type { IconName } from '../../../types/icons'; -import buildClassName from '../../util/buildClassName'; +import buildClassName from '../../../util/buildClassName'; type OwnProps = { name: IconName; diff --git a/src/components/common/icons/StarIcon.module.scss b/src/components/common/icons/StarIcon.module.scss new file mode 100644 index 000000000..29e2efd26 --- /dev/null +++ b/src/components/common/icons/StarIcon.module.scss @@ -0,0 +1,28 @@ +.root { + --color-fill: var(--color-primary); + + flex-shrink: 0; + display: flex; + width: 1rem; + height: 1rem; +} + +.middle { + width: 1.25rem; + height: 1.25rem; +} + +.big { + width: 1.5rem; + height: 1.5rem; +} + +.svg { + width: 100%; + height: 100%; +} + +.clickable { + cursor: var(--custom-cursor, pointer); + pointer-events: auto; +} diff --git a/src/components/common/icons/StarIcon.tsx b/src/components/common/icons/StarIcon.tsx new file mode 100644 index 000000000..2a24e6518 --- /dev/null +++ b/src/components/common/icons/StarIcon.tsx @@ -0,0 +1,139 @@ +import type { FC } from '../../../lib/teact/teact'; +import React, { memo } from '../../../lib/teact/teact'; + +import buildClassName from '../../../util/buildClassName'; + +import useUniqueId from '../../../hooks/useUniqueId'; + +import styles from './StarIcon.module.scss'; + +type OwnProps = { + type?: 'gold' | 'premium' | 'regular'; + size?: 'small' | 'middle' | 'big'; + className?: string; + onClick?: VoidFunction; +}; + +/* eslint-disable max-len */ +const STAR_PATH = 'M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z'; +const GOLD_STAR_PATH = 'M10.5197 16.2049L6.46899 18.6864C6.04779 18.9444 5.49716 18.8121 5.23913 18.3909C5.11311 18.1852 5.07554 17.9373 5.13494 17.7035L5.762 15.2354C5.98835 14.3444 6.59803 13.5997 7.42671 13.2018L11.8459 11.0801C12.0519 10.9812 12.1387 10.734 12.0398 10.528C11.9597 10.3611 11.7786 10.2677 11.5962 10.2993L6.67709 11.1509C5.67715 11.324 4.65172 11.0479 3.87392 10.3961L2.31994 9.09382C1.94135 8.77655 1.89164 8.21245 2.20891 7.83386C2.36321 7.64972 2.58511 7.53541 2.82462 7.51667L7.5725 7.1451C7.90793 7.11885 8.20025 6.90658 8.32901 6.59574L10.1607 2.17427C10.3497 1.71792 10.8729 1.50123 11.3292 1.69028C11.5484 1.78105 11.7225 1.95514 11.8132 2.17427L13.6449 6.59574C13.7736 6.90658 14.066 7.11885 14.4014 7.1451L19.1754 7.51871C19.6678 7.55725 20.0358 7.9877 19.9972 8.48015C19.9787 8.71704 19.8666 8.93682 19.6858 9.09098L16.0449 12.1949C15.7886 12.4134 15.6768 12.7574 15.7556 13.0849L16.8749 17.7348C16.9905 18.215 16.6949 18.698 16.2147 18.8137C15.9839 18.8692 15.7406 18.8307 15.5382 18.7068L11.4541 16.2049C11.1674 16.0292 10.8064 16.0292 10.5197 16.2049Z'; +/* eslint-enable max-len */ + +const StarIcon: FC = ({ + type = 'regular', + size = 'small', + className, + onClick, +}) => { + const randomId = useUniqueId(); + const validSvgRandomId = `svg-${randomId}`; // ID must start with a letter + + return ( + + {type === 'gold' + ? + : type === 'premium' + ? + : } + + ); +}; + +function GoldStarIcon({ randomId }: { randomId: string }) { + const fillId = `${randomId}-fill`; + const stroke1Id = `${randomId}-stroke1`; + const stroke2Id = `${randomId}-stroke2`; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function PremiumStarIcon({ randomId }: { randomId: string }) { + return ( + + + + + + + + + + + ); +} + +function RegularStarIcon() { + return ( + + + + ); +} + +export default memo(StarIcon); diff --git a/src/components/common/profile/BusinessHours.tsx b/src/components/common/profile/BusinessHours.tsx index 2f6733ecd..1c5b8d2cd 100644 --- a/src/components/common/profile/BusinessHours.tsx +++ b/src/components/common/profile/BusinessHours.tsx @@ -22,7 +22,7 @@ import useSelectorSignal from '../../../hooks/useSelectorSignal'; import ListItem from '../../ui/ListItem'; import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition'; -import Icon from '../Icon'; +import Icon from '../icons/Icon'; import styles from './BusinessHours.module.scss'; diff --git a/src/components/common/spoiler/Spoiler.tsx b/src/components/common/spoiler/Spoiler.tsx index 364625d6f..fa6c0aeb1 100644 --- a/src/components/common/spoiler/Spoiler.tsx +++ b/src/components/common/spoiler/Spoiler.tsx @@ -1,4 +1,4 @@ -import type { FC } from '../../../lib/teact/teact'; +import type { TeactNode } from '../../../lib/teact/teact'; import React, { memo, useEffect, useRef } from '../../../lib/teact/teact'; import { ApiMessageEntityTypes } from '../../../api/types'; @@ -11,7 +11,7 @@ import useLastCallback from '../../../hooks/useLastCallback'; import './Spoiler.scss'; type OwnProps = { - children?: React.ReactNode; + children?: TeactNode; containerId?: string; }; @@ -19,10 +19,10 @@ const revealByContainerId: Map = new Map(); const buildClassName = createClassNameBuilder('Spoiler'); -const Spoiler: FC = ({ +const Spoiler = ({ children, containerId, -}) => { +}: OwnProps) => { // eslint-disable-next-line no-null/no-null const contentRef = useRef(null); diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index ca5fae8bc..7343f6d64 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -89,7 +89,7 @@ color: var(--color-white); } - .VerifiedIcon, .PremiumIcon { + .VerifiedIcon, .StarIcon { --color-fill: #fff; --color-checkmark: var(--color-primary); } diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index 52fc056b2..aebeadb93 100644 --- a/src/components/left/main/LeftMainHeader.scss +++ b/src/components/left/main/LeftMainHeader.scss @@ -121,7 +121,7 @@ color: var(--color-primary); } - .PremiumIcon { + .StarIcon { width: 1.5rem; height: 1.5rem; } diff --git a/src/components/left/main/StatusButton.tsx b/src/components/left/main/StatusButton.tsx index b6145587a..f072c8d63 100644 --- a/src/components/left/main/StatusButton.tsx +++ b/src/components/left/main/StatusButton.tsx @@ -14,7 +14,7 @@ import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useFlag from '../../../hooks/useFlag'; import CustomEmoji from '../../common/CustomEmoji'; -import PremiumIcon from '../../common/PremiumIcon'; +import StarIcon from '../../common/icons/StarIcon'; import CustomEmojiEffect from '../../common/reactions/CustomEmojiEffect'; import Button from '../../ui/Button'; import StatusPickerMenu from './StatusPickerMenu.async'; @@ -82,7 +82,7 @@ const StatusButton: FC = ({ emojiStatus }) => { size={EMOJI_STATUS_SIZE} loopLimit={EMOJI_STATUS_LOOP_LIMIT} /> - ) : } + ) : } } + leftElement={} onClick={handleOpenPremiumModal} > {lang('PrivacyLastSeenPremium')} diff --git a/src/components/left/settings/PrivacyLockedOption.tsx b/src/components/left/settings/PrivacyLockedOption.tsx index ac159de12..1e2f26213 100644 --- a/src/components/left/settings/PrivacyLockedOption.tsx +++ b/src/components/left/settings/PrivacyLockedOption.tsx @@ -3,7 +3,7 @@ import { getActions } from '../../../global'; import useLang from '../../../hooks/useLang'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import styles from './PrivacyLockedOption.module.scss'; diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index a89a72a38..fe9641294 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -103,7 +103,7 @@ } } -.settings-main-menu-premium .PremiumIcon { +.settings-main-menu-star .StarIcon { margin-right: 1.25rem; } diff --git a/src/components/left/settings/SettingsExperimental.tsx b/src/components/left/settings/SettingsExperimental.tsx index d32d53a05..48bf911f4 100644 --- a/src/components/left/settings/SettingsExperimental.tsx +++ b/src/components/left/settings/SettingsExperimental.tsx @@ -78,7 +78,7 @@ const SettingsExperimental: FC = ({
requestConfetti({})} + onClick={() => requestConfetti({ withStars: true })} icon="animations" >
Launch some confetti!
diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 0420f5d14..f433b4432 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -9,13 +9,14 @@ import { selectIsGiveawayGiftsPurchaseAvailable, selectIsPremiumPurchaseBlocked, } from '../../../global/selectors'; +import { formatInteger } from '../../../util/textFormat'; import useFlag from '../../../hooks/useFlag'; import useHistoryBack from '../../../hooks/useHistoryBack'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import PremiumIcon from '../../common/PremiumIcon'; +import StarIcon from '../../common/icons/StarIcon'; import ChatExtra from '../../common/profile/ChatExtra'; import ProfileInfo from '../../common/ProfileInfo'; import ConfirmDialog from '../../ui/ConfirmDialog'; @@ -32,16 +33,20 @@ type StateProps = { currentUserId?: string; canBuyPremium?: boolean; isGiveawayAvailable?: boolean; + starsBalance?: number; + shouldDisplayStars?: boolean; }; const SettingsMain: FC = ({ isActive, - onScreenSelect, - onReset, currentUserId, sessionCount, canBuyPremium, isGiveawayAvailable, + starsBalance, + shouldDisplayStars, + onScreenSelect, + onReset, }) => { const { loadProfilePhotos, @@ -49,6 +54,7 @@ const SettingsMain: FC = ({ openSupportChat, openUrl, openPremiumGiftingModal, + openStarsBalanceModal, } = getActions(); const [isSupportDialogOpen, openSupportDialog, closeSupportDialog] = useFlag(false); @@ -156,18 +162,31 @@ const SettingsMain: FC = ({
{canBuyPremium && ( } - className="settings-main-menu-premium" + leftElement={} + className="settings-main-menu-star" // eslint-disable-next-line react/jsx-no-bind onClick={() => openPremiumModal()} > {lang('TelegramPremium')} )} + {shouldDisplayStars && ( + } + className="settings-main-menu-star" + // eslint-disable-next-line react/jsx-no-bind + onClick={() => openStarsBalanceModal({})} + > + {lang('MenuTelegramStars')} + {Boolean(starsBalance) && ( + {formatInteger(starsBalance)} + )} + + )} {isGiveawayAvailable && ( openPremiumGiftingModal()} > @@ -213,12 +232,16 @@ export default memo(withGlobal( (global): StateProps => { const { currentUserId } = global; const isGiveawayAvailable = selectIsGiveawayGiftsPurchaseAvailable(global); + const starsBalance = global.stars?.balance; + const shouldDisplayStars = Boolean(global.stars?.history?.all?.transactions.length); return { sessionCount: global.activeSessions.orderedHashes.length, currentUserId, canBuyPremium: !selectIsPremiumPurchaseBlocked(global), isGiveawayAvailable, + starsBalance, + shouldDisplayStars, }; }, )(SettingsMain)); diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index ef04596e9..563d573d5 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -11,7 +11,7 @@ import { selectCanSetPasscode, selectIsCurrentUserPremium } from '../../../globa import useHistoryBack from '../../../hooks/useHistoryBack'; import useLang from '../../../hooks/useLang'; -import PremiumIcon from '../../common/PremiumIcon'; +import StarIcon from '../../common/icons/StarIcon'; import Checkbox from '../../ui/Checkbox'; import ListItem from '../../ui/ListItem'; @@ -282,7 +282,7 @@ const SettingsPrivacy: FC = ({ } + rightElement={isCurrentUserPremium && } className="no-icon" // eslint-disable-next-line react/jsx-no-bind onClick={() => onScreenSelect(SettingsScreens.PrivacyVoiceMessages)} @@ -296,7 +296,7 @@ const SettingsPrivacy: FC = ({ } + rightElement={isCurrentUserPremium && } className="no-icon" // eslint-disable-next-line react/jsx-no-bind onClick={() => onScreenSelect(SettingsScreens.PrivacyMessages)} diff --git a/src/components/left/settings/SettingsPrivacyLastSeen.tsx b/src/components/left/settings/SettingsPrivacyLastSeen.tsx index 138a08370..3726204a7 100644 --- a/src/components/left/settings/SettingsPrivacyLastSeen.tsx +++ b/src/components/left/settings/SettingsPrivacyLastSeen.tsx @@ -9,7 +9,7 @@ import renderText from '../../common/helpers/renderText'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import PremiumIcon from '../../common/PremiumIcon'; +import StarIcon from '../../common/icons/StarIcon'; import Checkbox from '../../ui/Checkbox'; import ListItem from '../../ui/ListItem'; @@ -55,7 +55,7 @@ const SettingsPrivacyLastSeen = ({ )}
} + leftElement={} onClick={handleOpenPremiumModal} > {isCurrentUserPremium ? lang('PrivacyLastSeenPremiumForPremium') : lang('PrivacyLastSeenPremium')} diff --git a/src/components/main/AppendEntityPickerModal.tsx b/src/components/main/AppendEntityPickerModal.tsx index 9f2c4f7e1..c1fccb4af 100644 --- a/src/components/main/AppendEntityPickerModal.tsx +++ b/src/components/main/AppendEntityPickerModal.tsx @@ -21,7 +21,7 @@ import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import Picker from '../common/Picker'; import Button from '../ui/Button'; import ConfirmDialog from '../ui/ConfirmDialog'; diff --git a/src/components/main/ConfettiContainer.tsx b/src/components/main/ConfettiContainer.tsx index a7c3e00f7..7fd45fe8f 100644 --- a/src/components/main/ConfettiContainer.tsx +++ b/src/components/main/ConfettiContainer.tsx @@ -1,4 +1,3 @@ -import type { FC } from '../../lib/teact/teact'; import React, { memo, useRef } from '../../lib/teact/teact'; import { withGlobal } from '../../global'; @@ -32,6 +31,7 @@ interface Confetti { }; size: number; color: string; + isStar?: boolean; flicker: number; flickerFrequency: number; rotation: number; @@ -42,8 +42,11 @@ interface Confetti { const CONFETTI_FADEOUT_TIMEOUT = 10000; const DEFAULT_CONFETTI_SIZE = 10; const CONFETTI_COLORS = ['#E8BC2C', '#D0049E', '#02CBFE', '#5723FD', '#FE8C27', '#6CB859']; +// eslint-disable-next-line max-len +const STAR_PATH = new Path2D('M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z'); +const STAR_SIZE_MULTIPLIER = 1.5; -const ConfettiContainer: FC = ({ confetti }) => { +const ConfettiContainer = ({ confetti }: StateProps) => { // eslint-disable-next-line no-null/no-null const canvasRef = useRef(null); const confettiRef = useRef([]); @@ -76,6 +79,7 @@ const ConfettiContainer: FC = ({ confetti }) => { rotation: 0, lastDrawnAt: Date.now(), frameCount: 0, + isStar: confetti?.withStars && Math.random() > 0.8, }); } }); @@ -143,17 +147,29 @@ const ConfettiContainer: FC = ({ confetti }) => { confettiRef.current[i] = newConfetti; ctx.fillStyle = color; - ctx.beginPath(); - ctx.ellipse( - pos.x, - pos.y, - size, - flicker, - rotation, - 0, - 2 * Math.PI, - ); - ctx.fill(); + if (c.isStar) { + ctx.save(); + ctx.translate(pos.x, pos.y); + ctx.scale( + (size / DEFAULT_CONFETTI_SIZE) * STAR_SIZE_MULTIPLIER, + (size / DEFAULT_CONFETTI_SIZE) * STAR_SIZE_MULTIPLIER, + ); + ctx.rotate(rotation); + ctx.fill(STAR_PATH); + ctx.restore(); + } else { + ctx.beginPath(); + ctx.ellipse( + pos.x, + pos.y, + size, + flicker, + rotation, + 0, + 2 * Math.PI, + ); + ctx.fill(); + } }); confettiRef.current = confettiRef.current.filter((c) => !confettiToRemove.includes(c)); if (confettiRef.current.length) { diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 0bf221b27..5631060dc 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -248,6 +248,7 @@ const Main: FC = ({ loadSavedReactionTags, loadTimezones, loadQuickReplies, + loadStarStatus, } = getActions(); if (DEBUG && !DEBUG_isLogged) { @@ -328,6 +329,7 @@ const Main: FC = ({ loadSavedReactionTags(); loadTimezones(); loadQuickReplies(); + loadStarStatus(); } }, [isMasterTab, isSynced]); diff --git a/src/components/main/premium/GiftPremiumModal.tsx b/src/components/main/premium/GiftPremiumModal.tsx index 18c2eecb1..a86294fb0 100644 --- a/src/components/main/premium/GiftPremiumModal.tsx +++ b/src/components/main/premium/GiftPremiumModal.tsx @@ -22,7 +22,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import AvatarList from '../../common/AvatarList'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import Link from '../../ui/Link'; import Modal from '../../ui/Modal'; @@ -122,6 +122,7 @@ const GiftPremiumModal: FC = ({ left, width, height, + withStars: true, }); } }); diff --git a/src/components/main/premium/GiveawayModal.tsx b/src/components/main/premium/GiveawayModal.tsx index e6d444ea3..9103ed179 100644 --- a/src/components/main/premium/GiveawayModal.tsx +++ b/src/components/main/premium/GiveawayModal.tsx @@ -31,7 +31,7 @@ import useLastCallback from '../../../hooks/useLastCallback'; import CalendarModal from '../../common/CalendarModal'; import CountryPickerModal from '../../common/CountryPickerModal'; import GroupChatInfo from '../../common/GroupChatInfo'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import ConfirmDialog from '../../ui/ConfirmDialog'; import InputText from '../../ui/InputText'; diff --git a/src/components/main/premium/GiveawayTypeOption.tsx b/src/components/main/premium/GiveawayTypeOption.tsx index db6eb7c31..bc301fc5a 100644 --- a/src/components/main/premium/GiveawayTypeOption.tsx +++ b/src/components/main/premium/GiveawayTypeOption.tsx @@ -7,7 +7,7 @@ import buildClassName from '../../../util/buildClassName'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import styles from './GiveawayTypeOption.module.scss'; diff --git a/src/components/main/premium/PremiumGiftingModal.tsx b/src/components/main/premium/PremiumGiftingModal.tsx index 1a436f514..2e53ec410 100644 --- a/src/components/main/premium/PremiumGiftingModal.tsx +++ b/src/components/main/premium/PremiumGiftingModal.tsx @@ -15,7 +15,7 @@ import sortChatIds from '../../common/helpers/sortChatIds'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import Picker from '../../common/Picker'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; diff --git a/src/components/main/premium/PremiumMainModal.tsx b/src/components/main/premium/PremiumMainModal.tsx index 9a6026732..3117c93c9 100644 --- a/src/components/main/premium/PremiumMainModal.tsx +++ b/src/components/main/premium/PremiumMainModal.tsx @@ -202,6 +202,7 @@ const PremiumMainModal: FC = ({ left, width, height, + withStars: true, }); } }); diff --git a/src/components/main/premium/common/PremiumLimitReachedModal.tsx b/src/components/main/premium/common/PremiumLimitReachedModal.tsx index f0d967143..d2a059075 100644 --- a/src/components/main/premium/common/PremiumLimitReachedModal.tsx +++ b/src/components/main/premium/common/PremiumLimitReachedModal.tsx @@ -15,7 +15,7 @@ import renderText from '../../../common/helpers/renderText'; import useFlag from '../../../../hooks/useFlag'; import useLang from '../../../../hooks/useLang'; -import Icon from '../../../common/Icon'; +import Icon from '../../../common/icons/Icon'; import Button from '../../../ui/Button'; import Modal from '../../../ui/Modal'; import PremiumLimitsCompare from './PremiumLimitsCompare'; diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index 4b69e72ba..13971e415 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -102,7 +102,9 @@ const ActionMessage: FC = ({ observeIntersectionForPlaying, onPinnedIntersectionChange, }) => { - const { openPremiumModal, requestConfetti, checkGiftCode } = getActions(); + const { + openPremiumModal, requestConfetti, checkGiftCode, getReceipt, + } = getActions(); const lang = useLang(); @@ -150,7 +152,7 @@ const ActionMessage: FC = ({ useEffect(() => { if (isVisible && shouldShowConfettiRef.current) { shouldShowConfettiRef.current = false; - requestConfetti({}); + requestConfetti({ withStars: true }); } }, [isVisible, requestConfetti]); @@ -210,6 +212,15 @@ const ActionMessage: FC = ({ checkGiftCode({ slug, message: { chatId: message.chatId, messageId: message.id } }); }; + const handleClick = () => { + if (message.content.action?.type === 'receipt') { + getReceipt({ + chatId: message.chatId, + messageId: message.id, + }); + } + }; + // TODO Refactoring for action rendering const shouldSkipRender = isInsideTopic && message.content.action?.text === 'TopicWasCreatedAction'; if (shouldSkipRender) { @@ -294,7 +305,7 @@ const ActionMessage: FC = ({ onContextMenu={handleContextMenu} > {!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && ( - {renderContent()} + {renderContent()} )} {isGift && renderGift()} {isGiftCode && renderGiftCode()} diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index b35589e66..78e0a85bc 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -132,6 +132,7 @@ type StateProps = { const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000; +const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000; const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000; const BOTTOM_THRESHOLD = 50; const UNREAD_DIVIDER_TOP = 10; @@ -188,7 +189,7 @@ const MessageList: FC = ({ }) => { const { loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, - loadMessageViews, loadPeerStoriesByIds, + loadMessageViews, loadPeerStoriesByIds, loadFactChecks, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -320,6 +321,17 @@ const MessageList: FC = ({ loadMessageViews({ chatId, ids }); }, MESSAGE_COMMENTS_POLLING_INTERVAL, true); + useInterval(() => { + if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') { + return; + } + const ids = messageIds.filter((id) => messagesById[id].factCheck?.shouldFetch); + + if (!ids.length) return; + + loadFactChecks({ chatId, ids }); + }, MESSAGE_FACT_CHECK_UPDATE_INTERVAL); + const loadMoreAround = useMemo(() => { if (type !== 'thread') { return undefined; diff --git a/src/components/middle/PremiumRequiredMessage.tsx b/src/components/middle/PremiumRequiredMessage.tsx index 4af7b52c8..8479e36f0 100644 --- a/src/components/middle/PremiumRequiredMessage.tsx +++ b/src/components/middle/PremiumRequiredMessage.tsx @@ -10,7 +10,7 @@ import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import AnimatedIconWithPreview from '../common/AnimatedIconWithPreview'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import Button from '../ui/Button'; import styles from './PremiumRequiredMessage.module.scss'; diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx index 895932364..1fc8316b1 100644 --- a/src/components/middle/composer/AttachMenu.tsx +++ b/src/components/middle/composer/AttachMenu.tsx @@ -30,7 +30,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useMouseInside from '../../../hooks/useMouseInside'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton'; diff --git a/src/components/middle/composer/ComposerEmbeddedMessage.tsx b/src/components/middle/composer/ComposerEmbeddedMessage.tsx index 15fcb8628..4b85459ea 100644 --- a/src/components/middle/composer/ComposerEmbeddedMessage.tsx +++ b/src/components/middle/composer/ComposerEmbeddedMessage.tsx @@ -38,7 +38,7 @@ import useMenuPosition from '../../../hooks/useMenuPosition'; import useShowTransition from '../../../hooks/useShowTransition'; import { ClosableEmbeddedMessage } from '../../common/embedded/EmbeddedMessage'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; diff --git a/src/components/middle/message/Contact.module.scss b/src/components/middle/message/Contact.module.scss index fd7a5263d..6024059a1 100644 --- a/src/components/middle/message/Contact.module.scss +++ b/src/components/middle/message/Contact.module.scss @@ -1,21 +1,6 @@ .root { position: relative; margin: 0.25rem 0.25rem 0.875rem 0.25rem; - border-radius: 0.25rem; - overflow: hidden; - background-color: var(--accent-background-color); - color: var(--accent-color); - - &::before { - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - inset-inline-start: 0; - width: 0.1875rem; - background: var(--bar-gradient, var(--accent-color)); - } } .info-container { diff --git a/src/components/middle/message/FactCheck.module.scss b/src/components/middle/message/FactCheck.module.scss new file mode 100644 index 000000000..ffd553019 --- /dev/null +++ b/src/components/middle/message/FactCheck.module.scss @@ -0,0 +1,52 @@ +@use '../../../styles/mixins'; + +.root { + margin-top: 0.5rem; + font-size: 0.875rem; + padding-block: 0.25rem; + padding-inline-end: 0.25rem; +} + +.title { + font-weight: 500; +} + +.content { + color: var(--color-text); +} + +.separator { + --color-dividers: var(--accent-color); + + margin-block: 0.25rem; + opacity: 0.25; +} + +.footnote { + font-size: 0.75rem; +} + +.collapsed { + @include mixins.gradient-border-bottom(1rem); +} + +.cutoutWrapper { + max-height: inherit; +} + +.collapseIcon { + position: absolute; + display: grid; + place-items: center; + + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + + bottom: 0; + right: 0; +} + +.clickable { + cursor: var(--custom-cursor, pointer); +} diff --git a/src/components/middle/message/FactCheck.tsx b/src/components/middle/message/FactCheck.tsx new file mode 100644 index 000000000..27a410e52 --- /dev/null +++ b/src/components/middle/message/FactCheck.tsx @@ -0,0 +1,89 @@ +import React, { memo, useMemo, useRef } from '../../../lib/teact/teact'; + +import type { ApiFactCheck } from '../../../api/types'; + +import buildClassName from '../../../util/buildClassName'; +import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; + +import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import Icon from '../../common/icons/Icon'; +import PeerColorWrapper from '../../common/PeerColorWrapper'; +import Separator from '../../ui/Separator'; + +import styles from './FactCheck.module.scss'; + +type OwnProps = { + factCheck: ApiFactCheck; + isToggleDisabled?: boolean; +}; + +const COLOR = { + color: 0, +}; +const MAX_LINES = 4; + +const FactCheck = ({ factCheck, isToggleDisabled }: OwnProps) => { + const lang = useLang(); + + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + // eslint-disable-next-line no-null/no-null + const cutoutRef = useRef(null); + const { + isCollapsed, isCollapsible, setIsCollapsed, + } = useCollapsibleLines(ref, MAX_LINES, cutoutRef); + + const countryLocalized = useMemo(() => { + if (!factCheck.countryCode || !lang.code) return undefined; + + const displayNames = new Intl.DisplayNames([lang.code], { type: 'region' }); + return displayNames.of(factCheck.countryCode); + }, [factCheck.countryCode, lang.code]); + + const canExpand = !isToggleDisabled && isCollapsed; + + const handleExpand = useLastCallback(() => { + setIsCollapsed(false); + }); + + const handleToggle = useLastCallback(() => { + setIsCollapsed((prev) => !prev); + }); + + if (!factCheck.text) { + return undefined; + } + + return ( + +
+
{lang('FactCheck')}
+
+ {renderTextWithEntities({ + text: factCheck.text.text, + entities: factCheck.text.entities, + })} +
+ +
{lang('FactCheckFooter', countryLocalized)}
+
+ {isCollapsible && ( +
+ +
+ )} +
+ ); +}; + +export default memo(FactCheck); diff --git a/src/components/middle/message/Invoice.scss b/src/components/middle/message/Invoice.scss index 86706bfd9..111ca5a5c 100644 --- a/src/components/middle/message/Invoice.scss +++ b/src/components/middle/message/Invoice.scss @@ -38,16 +38,16 @@ top: 0; padding: 0.25rem 0.5rem; margin: 0.25rem; - background-color: rgba(0, 0, 0, 0.2); + background-color: rgba(0, 0, 0, 0.4); border-radius: var(--border-radius-messages-small); color: var(--color-white); font-weight: 500; - - span { - margin-left: 0.5rem; - } } } + + .test-invoice { + margin-left: 0.5rem; + } } .invoice-image-container { diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index 6f765c0bb..e557f7ee1 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -5,6 +5,7 @@ import type { ApiMessage } from '../../../api/types'; import type { ISettings } from '../../../types'; import { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config'; +import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { getMessageInvoice, getWebDocumentHash } from '../../../global/helpers'; import buildStyle from '../../../util/buildStyle'; import { formatCurrency } from '../../../util/formatCurrency'; @@ -67,8 +68,10 @@ const Invoice: FC = ({ if (photoUrl) { const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; getCustomAppendixBg(photoUrl, false, isSelected, theme).then((appendixBg) => { - contentEl.style.setProperty('--appendix-bg', appendixBg); - contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); + requestMutation(() => { + contentEl.style.setProperty('--appendix-bg', appendixBg); + contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); + }); }); } }, [shouldAffectAppendix, photoUrl, isInSelectMode, isSelected, theme]); @@ -117,7 +120,7 @@ const Invoice: FC = ({ )}

{formatCurrency(amount, currency, lang.code)} - {isTest && {lang('PaymentTestInvoice')}} + {isTest && {lang('PaymentTestInvoice')}}

diff --git a/src/components/middle/message/MentionLink.tsx b/src/components/middle/message/MentionLink.tsx index 1468efcb0..e517b3d92 100644 --- a/src/components/middle/message/MentionLink.tsx +++ b/src/components/middle/message/MentionLink.tsx @@ -1,4 +1,4 @@ -import type { FC } from '../../../lib/teact/teact'; +import type { TeactNode } from '../../../lib/teact/teact'; import React from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; @@ -12,19 +12,19 @@ import useAppLayout from '../../../hooks/useAppLayout'; type OwnProps = { userId?: string; username?: string; - children: React.ReactNode; + children: TeactNode; }; type StateProps = { userOrChat?: ApiPeer; }; -const MentionLink: FC = ({ +const MentionLink = ({ userId, username, userOrChat, children, -}) => { +}: OwnProps & StateProps) => { const { openChat, openChatByUsername, diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index f63eb254c..747392adc 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -145,9 +145,9 @@ import DotAnimation from '../../common/DotAnimation'; import EmbeddedMessage from '../../common/embedded/EmbeddedMessage'; import EmbeddedStory from '../../common/embedded/EmbeddedStory'; import FakeIcon from '../../common/FakeIcon'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; +import StarIcon from '../../common/icons/StarIcon'; import MessageText from '../../common/MessageText'; -import PremiumIcon from '../../common/PremiumIcon'; import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; import TopicChip from '../../common/TopicChip'; import Button from '../../ui/Button'; @@ -157,6 +157,7 @@ import AnimatedEmoji from './AnimatedEmoji'; import CommentButton from './CommentButton'; import Contact from './Contact'; import ContextMenuContainer from './ContextMenuContainer.async'; +import FactCheck from './FactCheck'; import Game from './Game'; import Giveaway from './Giveaway'; import InlineButtons from './InlineButtons'; @@ -470,7 +471,7 @@ const Message: FC = ({ ); const { - id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, + id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck, } = message; useEffect(() => { @@ -524,6 +525,8 @@ const Message: FC = ({ const noUserColors = isOwn && !isCustomShape; + const hasFactCheck = Boolean(factCheck?.text); + const hasSubheader = hasTopicChip || hasMessageReply || hasStoryReply; const selectMessage = useLastCallback((e?: React.MouseEvent, groupedId?: string) => { @@ -627,12 +630,13 @@ const Message: FC = ({ }, [focusLastMessage, isLastInList, transcribedText, withVoiceTranscription]); const textMessage = album?.hasMultipleCaptions ? undefined : (album?.captionMessage || message); - const hasText = textMessage && hasMessageText(textMessage); + const hasTextContent = textMessage && hasMessageText(textMessage); + const hasText = hasTextContent || hasFactCheck; const containerClassName = buildClassName( 'Message message-list-item', isFirstInGroup && 'first-in-group', - isProtected && !hasText ? 'is-protected' : 'allow-selection', + isProtected && !hasTextContent ? 'is-protected' : 'allow-selection', isLastInGroup && 'last-in-group', isFirstInDocumentGroup && 'first-in-document-group', isLastInDocumentGroup && 'last-in-document-group', @@ -915,6 +919,7 @@ const Message: FC = ({ observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForPlaying={observeIntersectionForPlaying} withTranslucentThumbs={isCustomShape} + isInSelectMode={isInSelectMode} /> ); } @@ -1227,6 +1232,9 @@ const Message: FC = ({
)} + {hasFactCheck && ( + + )} {metaPosition === 'in-text' && renderReactionsAndMeta()} )} @@ -1331,7 +1339,7 @@ const Message: FC = ({ observeIntersectionForPlaying={observeIntersectionForPlaying} /> )} - {!asForwarded && !senderEmojiStatus && senderIsPremium && } + {!asForwarded && !senderEmojiStatus && senderIsPremium && } {senderPeer?.fakeType && } ) : !botSender ? ( diff --git a/src/components/middle/message/RoundVideo.tsx b/src/components/middle/message/RoundVideo.tsx index b247c6a7b..c403f39ae 100644 --- a/src/components/middle/message/RoundVideo.tsx +++ b/src/components/middle/message/RoundVideo.tsx @@ -31,7 +31,7 @@ import useShowTransition from '../../../hooks/useShowTransition'; import useSignal from '../../../hooks/useSignal'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import MediaSpoiler from '../../common/MediaSpoiler'; import Button from '../../ui/Button'; import OptimizedVideo from '../../ui/OptimizedVideo'; diff --git a/src/components/middle/message/SimilarChannels.tsx b/src/components/middle/message/SimilarChannels.tsx index b96839502..6304dde83 100644 --- a/src/components/middle/message/SimilarChannels.tsx +++ b/src/components/middle/message/SimilarChannels.tsx @@ -21,7 +21,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import Avatar from '../../common/Avatar'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import Skeleton from '../../ui/placeholder/Skeleton'; diff --git a/src/components/middle/message/SponsoredMessage.tsx b/src/components/middle/message/SponsoredMessage.tsx index 8ffd76dff..fdc75ada3 100644 --- a/src/components/middle/message/SponsoredMessage.tsx +++ b/src/components/middle/message/SponsoredMessage.tsx @@ -19,7 +19,7 @@ import useLastCallback from '../../../hooks/useLastCallback'; import AboutAdsModal from '../../common/AboutAdsModal.async'; import Avatar from '../../common/Avatar'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import PeerColorWrapper from '../../common/PeerColorWrapper'; import Button from '../../ui/Button'; import MessageAppendix from './MessageAppendix'; diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index f40d6d45d..3558b6d4d 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -322,7 +322,7 @@ color: var(--accent-color); } - .PremiumIcon { + .StarIcon { --color-fill: var(--accent-color); vertical-align: middle; opacity: 0.5; @@ -963,12 +963,6 @@ font-size: 0.875rem; } -// Remove extra bottom padding from `blockquote` -.text-entity-blockquote-wrapper { - display: inline-block; - width: 100%; -} - blockquote, .blockquote { display: inline-block; position: relative; diff --git a/src/components/middle/message/helpers/buildContentClassName.ts b/src/components/middle/message/helpers/buildContentClassName.ts index 73ee88948..87213d6f8 100644 --- a/src/components/middle/message/helpers/buildContentClassName.ts +++ b/src/components/middle/message/helpers/buildContentClassName.ts @@ -40,10 +40,11 @@ export function buildContentClassName( giveaway, giveawayResults, } = getMessageContent(message); const text = album?.hasMultipleCaptions ? undefined : getMessageContent(album?.captionMessage || message).text; + const hasFactCheck = Boolean(message.factCheck?.text); const classNames = [MESSAGE_CONTENT_CLASS_NAME]; const isMedia = storyData || photo || video || location || invoice?.extendedMedia; - const hasText = text || location?.type === 'venue' || isGeoLiveActive; + const hasText = text || location?.type === 'venue' || isGeoLiveActive || hasFactCheck; const isMediaWithNoText = isMedia && !hasText; const isViaBot = Boolean(message.viaBotId); @@ -144,7 +145,7 @@ export function buildContentClassName( classNames.push('has-background'); } - if (hasSubheader || asForwarded || isViaBot || !isMediaWithNoText || forceSenderName) { + if (hasSubheader || asForwarded || isViaBot || !isMediaWithNoText || forceSenderName || hasFactCheck) { classNames.push('has-solid-background'); } diff --git a/src/components/middle/message/reactions/ReactionSelectorCustomReaction.tsx b/src/components/middle/message/reactions/ReactionSelectorCustomReaction.tsx index 447bf8b7a..ee295add0 100644 --- a/src/components/middle/message/reactions/ReactionSelectorCustomReaction.tsx +++ b/src/components/middle/message/reactions/ReactionSelectorCustomReaction.tsx @@ -7,7 +7,7 @@ import buildClassName from '../../../../util/buildClassName'; import { REM } from '../../../common/helpers/mediaDimensions'; import CustomEmoji from '../../../common/CustomEmoji'; -import Icon from '../../../common/Icon'; +import Icon from '../../../common/icons/Icon'; import styles from './ReactionSelectorReaction.module.scss'; diff --git a/src/components/middle/message/reactions/ReactionSelectorReaction.tsx b/src/components/middle/message/reactions/ReactionSelectorReaction.tsx index 9c6419a95..d9e8d3970 100644 --- a/src/components/middle/message/reactions/ReactionSelectorReaction.tsx +++ b/src/components/middle/message/reactions/ReactionSelectorReaction.tsx @@ -10,7 +10,7 @@ import useFlag from '../../../../hooks/useFlag'; import useMedia from '../../../../hooks/useMedia'; import AnimatedSticker from '../../../common/AnimatedSticker'; -import Icon from '../../../common/Icon'; +import Icon from '../../../common/icons/Icon'; import styles from './ReactionSelectorReaction.module.scss'; diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index aaef1531f..71955d74d 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -15,6 +15,8 @@ import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async'; import MapModal from './map/MapModal.async'; import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async'; import ReportAdModal from './reportAd/ReportAdModal.async'; +import StarsBalanceModal from './stars/StarsBalanceModal.async'; +import StarsPaymentModal from './stars/StarsPaymentModal.async'; import UrlAuthModal from './urlAuth/UrlAuthModal.async'; import WebAppModal from './webApp/WebAppModal.async'; @@ -30,6 +32,8 @@ type ModalKey = keyof Pick; @@ -57,6 +61,8 @@ const MODALS: ModalRegistry = { webApp: WebAppModal, collectibleInfoModal: CollectibleInfoModal, mapModal: MapModal, + isStarPaymentModalOpen: StarsPaymentModal, + starsBalanceModal: StarsBalanceModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/modals/boost/BoostModal.tsx b/src/components/modals/boost/BoostModal.tsx index 2b383b96f..829b9451c 100644 --- a/src/components/modals/boost/BoostModal.tsx +++ b/src/components/modals/boost/BoostModal.tsx @@ -17,7 +17,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import Avatar from '../../common/Avatar'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import PremiumProgress from '../../common/PremiumProgress'; import Button from '../../ui/Button'; import ConfirmDialog from '../../ui/ConfirmDialog'; diff --git a/src/components/modals/collectible/CollectibleInfoModal.tsx b/src/components/modals/collectible/CollectibleInfoModal.tsx index 2185e9cd5..55014b504 100644 --- a/src/components/modals/collectible/CollectibleInfoModal.tsx +++ b/src/components/modals/collectible/CollectibleInfoModal.tsx @@ -20,7 +20,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import PickerSelectedItem from '../../common/PickerSelectedItem'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; diff --git a/src/components/modals/common/TableInfoModal.module.scss b/src/components/modals/common/TableInfoModal.module.scss new file mode 100644 index 000000000..19cb3ead6 --- /dev/null +++ b/src/components/modals/common/TableInfoModal.module.scss @@ -0,0 +1,37 @@ +.content { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: min(92vh, 40rem) !important; + overflow-x: hidden; +} + +.title { + background-color: var(--color-background-secondary); +} + +.value { + word-break: break-word; +} + +.table .cell { + border: 1px solid var(--color-borders); + padding: 0.25rem 0.5rem; +} + +.logo { + width: 6.25rem; + height: 6.25rem; + align-self: center; +} + +.avatar { + align-self: center; +} + +.chatItem { + margin: 0; + width: fit-content; + background-color: var(--color-background); + color: var(--color-primary); +} diff --git a/src/components/modals/common/TableInfoModal.tsx b/src/components/modals/common/TableInfoModal.tsx new file mode 100644 index 000000000..9381ecd99 --- /dev/null +++ b/src/components/modals/common/TableInfoModal.tsx @@ -0,0 +1,100 @@ +import React, { memo, type TeactNode } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import type { ApiPeer, ApiWebDocument } from '../../../api/types'; +import type { CustomPeer } from '../../../types'; + +import buildClassName from '../../../util/buildClassName'; + +import useLastCallback from '../../../hooks/useLastCallback'; + +import Avatar from '../../common/Avatar'; +import PickerSelectedItem from '../../common/PickerSelectedItem'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; + +import styles from './TableInfoModal.module.scss'; + +type ChatItem = { chatId: string }; + +export type TableData = [TeactNode, TeactNode | ChatItem][]; + +type OwnProps = { + isOpen?: boolean; + title?: string; + tableData?: TableData; + headerImageUrl?: string; + headerAvatarPeer?: ApiPeer | CustomPeer; + headerAvatarWebPhoto?: ApiWebDocument; + header?: TeactNode; + footer?: TeactNode; + buttonText?: string; + onClose: NoneToVoidFunction; + onButtonClick?: NoneToVoidFunction; +}; + +const TableInfoModal = ({ + isOpen, + title, + tableData, + headerImageUrl, + headerAvatarPeer, + headerAvatarWebPhoto, + header, + footer, + buttonText, + onClose, + onButtonClick, +}: OwnProps) => { + const { openChat } = getActions(); + const handleOpenChat = useLastCallback((peerId: string) => { + openChat({ id: peerId }); + onClose(); + }); + + const withAvatar = Boolean(headerAvatarPeer || headerAvatarWebPhoto); + + return ( + + {withAvatar ? ( + + ) : ( + + )} + {header} + + {tableData?.map(([label, value]) => ( + + + + + ))} +
{label} + {typeof value === 'object' && 'chatId' in value ? ( + + ) : value} +
+ {footer} + {buttonText && ( + + )} +
+ ); +}; + +export default memo(TableInfoModal); diff --git a/src/components/modals/giftcode/GiftCodeModal.module.scss b/src/components/modals/giftcode/GiftCodeModal.module.scss index 473cee6ae..15efed847 100644 --- a/src/components/modals/giftcode/GiftCodeModal.module.scss +++ b/src/components/modals/giftcode/GiftCodeModal.module.scss @@ -1,38 +1,8 @@ -.content { - display: flex; - flex-direction: column; - gap: 0.5rem; - max-height: min(92vh, 40rem) !important; - overflow-x: hidden; -} - .clickable { color: var(--color-primary); cursor: pointer; } -.title { - background-color: var(--color-background-secondary); -} - -.table td { - border: 1px solid var(--color-borders); - padding: 0.25rem 0.5rem; -} - -.chat-item { - margin: 0; - width: fit-content; - background-color: var(--color-background); - color: var(--color-primary); -} - -.logo { - width: 6.25rem; - height: 6.25rem; - align-self: center; -} - .centered { text-align: center !important; } diff --git a/src/components/modals/giftcode/GiftCodeModal.tsx b/src/components/modals/giftcode/GiftCodeModal.tsx index 3ede0acab..d1ef37261 100644 --- a/src/components/modals/giftcode/GiftCodeModal.tsx +++ b/src/components/modals/giftcode/GiftCodeModal.tsx @@ -1,4 +1,4 @@ -import React, { memo } from '../../../lib/teact/teact'; +import React, { memo, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiPeer } from '../../../api/types'; @@ -14,9 +14,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import LinkField from '../../common/LinkField'; -import PickerSelectedItem from '../../common/PickerSelectedItem'; -import Button from '../../ui/Button'; -import Modal from '../../ui/Modal'; +import TableInfoModal, { type TableData } from '../common/TableInfoModal'; import styles from './GiftCodeModal.module.scss'; @@ -39,18 +37,13 @@ const GiftCodeModal = ({ messageSender, }: OwnProps & StateProps) => { const { - closeGiftCodeModal, openChat, applyGiftCode, focusMessage, + closeGiftCodeModal, applyGiftCode, focusMessage, } = getActions(); const lang = useLang(); const isOpen = Boolean(modal); const canUse = (!modal?.info.toId || modal?.info.toId === currentUserId) && !modal?.info.usedAt; - const handleOpenChat = useLastCallback((peerId: string) => { - openChat({ id: peerId }); - closeGiftCodeModal(); - }); - const handleOpenGiveaway = useLastCallback(() => { if (!modal || !modal.info.giveawayMessageId) return; focusMessage({ @@ -68,101 +61,63 @@ const GiftCodeModal = ({ closeGiftCodeModal(); }); - function renderContent() { + const modalData = useMemo(() => { if (!modal) return undefined; const { slug, info } = modal; const fromId = info.fromId || messageSender?.id; - return ( + const header = ( <> -

{renderText(lang('lng_gift_link_about'), ['simple_markdown'])}

- - - - - - - - - - - - - - - - - - - - - -
{lang('BoostingFrom')} - {fromId ? ( - - ) : lang('BoostingNoRecipient')} -
- {lang('BoostingTo')} - - {info.toId ? ( - - ) : lang('BoostingNoRecipient')} -
- {lang('BoostingGift')} - - {lang('BoostingTelegramPremiumFor', lang('Months', info.months, 'i'))} -
- {lang('BoostingReason')} - - {info.isFromGiveaway && !info.toId ? lang('BoostingIncompleteGiveaway') - : lang(info.isFromGiveaway ? 'BoostingGiveaway' : 'BoostingYouWereSelected')} -
- {lang('BoostingDate')} - - {formatDateTimeToString(info.date * 1000, lang.code, true)} -
- - {renderText( - info.usedAt ? lang('BoostingUsedLinkDate', formatDateTimeToString(info.usedAt * 1000, lang.code, true)) - : lang('BoostingSendLinkToAnyone'), - ['simple_markdown'], - )} - - ); - } + + const tableData = [ + [lang('BoostingFrom'), fromId ? { chatId: fromId } : lang('BoostingNoRecipient')], + [lang('BoostingTo'), info.toId ? { chatId: info.toId } : lang('BoostingNoRecipient')], + [lang('BoostingGift'), lang('BoostingTelegramPremiumFor', lang('Months', info.months, 'i'))], + [lang('BoostingReason'), ( + + {info.isFromGiveaway && !info.toId ? lang('BoostingIncompleteGiveaway') + : lang(info.isFromGiveaway ? 'BoostingGiveaway' : 'BoostingYouWereSelected')} + + )], + [lang('BoostingDate'), formatDateTimeToString(info.date * 1000, lang.code, true)], + ] satisfies TableData; + + const footer = ( + + {renderText( + info.usedAt ? lang('BoostingUsedLinkDate', formatDateTimeToString(info.usedAt * 1000, lang.code, true)) + : lang('BoostingSendLinkToAnyone'), + ['simple_markdown'], + )} + + ); + + return { + header, + tableData, + footer, + }; + }, [lang, messageSender?.id, modal]); + + if (!modalData) return undefined; return ( - - {renderContent()} - + /> ); }; diff --git a/src/components/modals/reportAd/ReportAdModal.tsx b/src/components/modals/reportAd/ReportAdModal.tsx index dc7d12cf5..466f2b699 100644 --- a/src/components/modals/reportAd/ReportAdModal.tsx +++ b/src/components/modals/reportAd/ReportAdModal.tsx @@ -11,7 +11,7 @@ import buildClassName from '../../../util/buildClassName'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import SafeLink from '../../common/SafeLink'; import Button from '../../ui/Button'; import ListItem from '../../ui/ListItem'; diff --git a/src/components/modals/stars/BalanceBlock.tsx b/src/components/modals/stars/BalanceBlock.tsx new file mode 100644 index 000000000..ddecfaa94 --- /dev/null +++ b/src/components/modals/stars/BalanceBlock.tsx @@ -0,0 +1,31 @@ +import React, { memo } from '../../../lib/teact/teact'; + +import buildClassName from '../../../util/buildClassName'; +import { formatInteger } from '../../../util/textFormat'; + +import useLang from '../../../hooks/useLang'; + +import StarIcon from '../../common/icons/StarIcon'; + +import styles from './StarsBalanceModal.module.scss'; + +type OwnProps = { + balance: number; + className?: string; +}; + +const BalanceBlock = ({ balance, className }: OwnProps) => { + const lang = useLang(); + + return ( +
+ {lang('StarsBalance')} +
+ + {formatInteger(balance)} +
+
+ ); +}; + +export default memo(BalanceBlock); diff --git a/src/components/modals/stars/StarsBalanceModal.async.tsx b/src/components/modals/stars/StarsBalanceModal.async.tsx new file mode 100644 index 000000000..3a8f6892f --- /dev/null +++ b/src/components/modals/stars/StarsBalanceModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './StarsBalanceModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const StarsBalanceModalAsync: FC = (props) => { + const { modal } = props; + const StarsBalanceModal = useModuleLoader(Bundles.Extra, 'StarsBalanceModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return StarsBalanceModal ? : undefined; +}; + +export default StarsBalanceModalAsync; diff --git a/src/components/modals/stars/StarsBalanceModal.module.scss b/src/components/modals/stars/StarsBalanceModal.module.scss new file mode 100644 index 000000000..72060ea24 --- /dev/null +++ b/src/components/modals/stars/StarsBalanceModal.module.scss @@ -0,0 +1,239 @@ +@use '../../../styles/mixins'; + +.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; +} + +.section { + display: flex; + flex-direction: column; + align-items: center; + + padding: 0.5rem; + position: relative; + + @include mixins.adapt-padding-to-scrollbar(0.5rem); + + @include mixins.side-panel-section; +} + +.secondaryInfo { + font-size: 0.875rem; + color: var(--color-text-secondary); + background-color: var(--color-background-secondary); + padding: 0.5rem 1rem; +} + +.logo { + margin: 1rem; + width: 6.25rem; + height: 6.25rem; + min-height: 6.25rem; +} + +.logoBackground { + position: absolute; + top: 0.75rem; + left: 50%; + transform: translateX(-50%); + height: 8rem; +} + +.headerHext { + font-size: 1.5rem; + font-weight: 500; + text-align: center; + margin-inline: 0.5rem; +} + +.description { + text-align: center; + margin-inline: 0.5rem; + margin-bottom: 1rem; + text-wrap: balance; +} + +.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; +} + +.balance { + display: flex; + flex-direction: column; + align-items: flex-end; + line-height: 1; +} + +.smallerText { + font-size: 0.875rem; +} + +.balanceBottom { + font-weight: 500; + display: flex; + gap: 0.25rem; + line-height: 1.5; +} + +.modalBalance { + position: absolute; + top: 0.75rem; + right: 1.25rem; + z-index: 3; +} + +.options { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + 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); + } +} + +.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)); +} + +.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; +} + +.paymentContent { + display: flex; + flex-direction: column; + align-items: center; +} + +.paymentImages { + display: grid; + grid-auto-columns: 3.5rem; + grid-auto-flow: column; + place-items: center; + margin-bottom: 1rem; + position: relative; +} + +.paymentPhoto { + outline: 0.25rem solid var(--color-background); + z-index: 1; +} + +.paymentImageBackground { + height: 7rem; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.paymentAmount { + display: flex; + line-height: 1.125; + gap: 0.125rem; +} + +.paymentButton { + display: flex; + gap: 0.125rem; +} + +.paymentButtonStar { + --color-fill: white !important; +} + +.transactions { + display: flex; + flex-direction: column; +} diff --git a/src/components/modals/stars/StarsBalanceModal.tsx b/src/components/modals/stars/StarsBalanceModal.tsx new file mode 100644 index 000000000..13517e6fa --- /dev/null +++ b/src/components/modals/stars/StarsBalanceModal.tsx @@ -0,0 +1,251 @@ +import React, { + memo, useEffect, useMemo, useState, +} from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +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 buildClassName from '../../../util/buildClassName'; +import { formatCurrency } from '../../../util/formatCurrency'; +import renderText from '../../common/helpers/renderText'; + +import useFlag from '../../../hooks/useFlag'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import Icon from '../../common/icons/Icon'; +import StarIcon from '../../common/icons/StarIcon'; +import SafeLink from '../../common/SafeLink'; +import Button from '../../ui/Button'; +import InfiniteScroll from '../../ui/InfiniteScroll'; +import Modal from '../../ui/Modal'; +import TabList, { type TabWithProperties } from '../../ui/TabList'; +import Transition from '../../ui/Transition'; +import BalanceBlock from './BalanceBlock'; +import TransactionItem from './StarsTransactionItem'; + +import styles from './StarsBalanceModal.module.scss'; + +import StarLogo from '../../../assets/icons/StarLogo.svg'; +import StarsBackground from '../../../assets/stars-bg.png'; + +const TRANSACTION_TYPES = ['all', 'inbound', 'outbound'] as const; + +const TRANSACTION_TABS: TabWithProperties[] = [ + { title: 'StarsTransactionsAll' }, + { title: 'StarsTransactionsIncoming' }, + { title: 'StarsTransactionsOutgoing' }, +]; + +export type OwnProps = { + modal: TabState['starsBalanceModal']; +}; + +type StateProps = { + starsBalanceState?: GlobalState['stars']; + originPaymentBot?: ApiUser; +}; + +const StarsBalanceModal = ({ + modal, starsBalanceState, originPaymentBot, +}: OwnProps & StateProps) => { + const { closeStarsBalanceModal, loadStarsTransactions, openInvoice } = getActions(); + + const { balance, history, topupOptions } = starsBalanceState || {}; + + const lang = useLang(); + + const [isHeaderHidden, setHeaderHidden] = useState(true); + const [areOptionsExtended, markOptionsExtended, unmarkOptionsExtended] = useFlag(); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const isOpen = Boolean(modal && starsBalanceState); + + const productStarsPrice = modal?.originPayment?.invoice?.amount; + const starsNeeded = productStarsPrice ? productStarsPrice - (balance || 0) : undefined; + const originBotName = originPaymentBot && getUserFullName(originPaymentBot); + const shouldShowTransactions = Boolean(history?.all?.transactions.length && !modal?.originPayment); + + useEffect(() => { + if (!isOpen) { + setHeaderHidden(true); + unmarkOptionsExtended(); + setSelectedTabIndex(0); + } + }, [isOpen]); + + const renderingOptions = useMemo(() => { + if (!topupOptions) { + return undefined; + } + + const result: { option: ApiStarTopupOption; starsCount: number }[] = []; + let currentStarsCount = 0; + topupOptions.forEach((option) => { + if (!option.isExtended) currentStarsCount++; + if (!areOptionsExtended && option.isExtended) return; + if (starsNeeded && option.stars < starsNeeded) return; + result.push({ + option, + starsCount: currentStarsCount, + }); + }); + + return result; + }, [areOptionsExtended, topupOptions, starsNeeded]); + + const tosText = useMemo(() => { + if (!isOpen) return undefined; + + const text = lang('lng_credits_summary_options_about'); + const parts = text.split('{link}'); + return [ + parts[0], + , + parts[1], + ]; + }, [isOpen, lang]); + + function handleScroll(e: React.UIEvent) { + const { scrollTop } = e.currentTarget; + + setHeaderHidden(scrollTop <= 150); + } + + const handleClick = useLastCallback((option: ApiStarTopupOption) => { + openInvoice({ + type: 'stars', + option, + }); + }); + + const handleLoadMore = useLastCallback(() => { + loadStarsTransactions({ + type: TRANSACTION_TYPES[selectedTabIndex], + }); + }); + + return ( + +
+ + +
+

+ {lang('TelegramStars')} +

+
+
+ + +

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

+
+ {renderText( + starsNeeded ? lang('StarsNeededText', originBotName) : lang('TelegramStarsInfo'), + ['simple_markdown', 'emoji'], + )} +
+
+ {renderingOptions?.map(({ option, starsCount }) => ( + + ))} + {!areOptionsExtended && ( + + )} +
+
+
+ {tosText} +
+ {shouldShowTransactions && ( + <> + +
+ + + {history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions.map((transaction) => ( + + ))} + + +
+ + )} +
+
+ ); +}; + +function StarTopupOption({ + option, starsCount, onClick, +}: { + option: ApiStarTopupOption; starsCount: number; onClick?: (option: ApiStarTopupOption) => void; +}) { + const lang = useLang(); + + return ( +
onClick?.(option)}> +
+ +{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; + const bot = botId ? selectUser(global, botId) : undefined; + + return { + starsBalanceState: global.stars, + originPaymentBot: bot, + }; + }, +)(StarsBalanceModal)); diff --git a/src/components/modals/stars/StarsPaymentModal.async.tsx b/src/components/modals/stars/StarsPaymentModal.async.tsx new file mode 100644 index 000000000..a9ab95952 --- /dev/null +++ b/src/components/modals/stars/StarsPaymentModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './StarsPaymentModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const StarPaymentModalAsync: FC = (props) => { + const { modal } = props; + const StarPaymentModal = useModuleLoader(Bundles.Extra, 'StarPaymentModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return StarPaymentModal ? : undefined; +}; + +export default StarPaymentModalAsync; diff --git a/src/components/modals/stars/StarsPaymentModal.tsx b/src/components/modals/stars/StarsPaymentModal.tsx new file mode 100644 index 000000000..e41888a15 --- /dev/null +++ b/src/components/modals/stars/StarsPaymentModal.tsx @@ -0,0 +1,123 @@ +import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { ApiUser } from '../../../api/types'; +import type { GlobalState, TabState } from '../../../global/types'; + +import { getUserFullName } from '../../../global/helpers'; +import { selectTabState, selectUser } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; +import renderText from '../../common/helpers/renderText'; + +import useFlag from '../../../hooks/useFlag'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import Avatar from '../../common/Avatar'; +import StarIcon from '../../common/icons/StarIcon'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; +import BalanceBlock from './BalanceBlock'; + +import styles from './StarsBalanceModal.module.scss'; + +import StarsBackground from '../../../assets/stars-bg.png'; + +export type OwnProps = { + modal: TabState['isStarPaymentModalOpen']; +}; + +type StateProps = { + payment?: TabState['payment']; + starsBalanceState?: GlobalState['stars']; + bot?: ApiUser; +}; + +const StarPaymentModal = ({ + modal, bot, starsBalanceState, payment, +}: OwnProps & StateProps) => { + const { closePaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions(); + const [isLoading, markLoading, unmarkLoading] = useFlag(); + const isOpen = Boolean(modal && starsBalanceState); + + const photo = payment?.invoice?.photo; + + const lang = useLang(); + + useEffect(() => { + if (!isOpen) { + unmarkLoading(); + } + }, [isOpen]); + + const descriptionText = useMemo(() => { + if (!payment?.invoice) { + return ''; + } + + const botName = getUserFullName(bot); + const starsText = lang('Stars.Intro.PurchasedText.Stars', payment.invoice.amount); + + return lang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]); + }, [bot, payment, lang]); + + const handlePayment = useLastCallback(() => { + const price = payment?.invoice?.amount; + const balance = starsBalanceState?.balance; + if (price === undefined || balance === undefined) { + return; + } + + if (price > balance) { + openStarsBalanceModal({ + originPayment: payment, + }); + return; + } + + sendStarPaymentForm(); + markLoading(); + }); + + return ( + + +
+ + {photo && } + +
+

+ {lang('StarsConfirmPurchaseTitle')} +

+
+ {renderText(descriptionText, ['simple_markdown', 'emoji'])} +
+ +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const payment = selectTabState(global).payment; + const bot = payment?.botId ? selectUser(global, payment.botId) : undefined; + return { + bot, + starsBalanceState: global.stars, + payment, + }; + }, +)(StarPaymentModal)); diff --git a/src/components/modals/stars/StarsTransactionItem.module.scss b/src/components/modals/stars/StarsTransactionItem.module.scss new file mode 100644 index 000000000..21bc9b48f --- /dev/null +++ b/src/components/modals/stars/StarsTransactionItem.module.scss @@ -0,0 +1,54 @@ +.root { + display: flex; + gap: 0.75rem; + padding: 0.25rem; + border-radius: 0.5rem; + cursor: var(--custom-cursor, pointer); + transition: background-color 0.25s ease-out; + + &:hover { + background-color: var(--color-chat-hover); + } +} + +.info { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.stars { + display: flex; + align-items: center; + flex-shrink: 0; + gap: 0.25rem; +} + +.amount { + font-weight: 500; +} + +.title, .description, .date { + margin-bottom: 0; +} + +.title { + font-size: 1rem; +} + +.description { + font-size: 0.875rem; +} + +.date { + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.positive { + color: var(--color-success); +} + +.negative { + color: var(--color-error); +} diff --git a/src/components/modals/stars/StarsTransactionItem.tsx b/src/components/modals/stars/StarsTransactionItem.tsx new file mode 100644 index 000000000..b5e71fc6d --- /dev/null +++ b/src/components/modals/stars/StarsTransactionItem.tsx @@ -0,0 +1,102 @@ +import React, { memo, useMemo } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; + +import type { + ApiPeer, + ApiStarsTransaction, +} from '../../../api/types'; +import type { GlobalState } from '../../../global/types'; +import type { CustomPeer } from '../../../types'; + +import { getSenderTitle } from '../../../global/helpers'; +import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../global/helpers/payments'; +import { selectPeer } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; +import { formatDateTimeToString } from '../../../util/date/dateFormat'; +import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useSelector from '../../../hooks/useSelector'; + +import Avatar from '../../common/Avatar'; +import StarIcon from '../../common/icons/StarIcon'; + +import styles from './StarsTransactionItem.module.scss'; + +type OwnProps = { + transaction: ApiStarsTransaction; +}; + +function selectOptionalPeer(peerId?: string) { + return (global: GlobalState) => ( + peerId ? selectPeer(global, peerId) : undefined + ); +} + +const StarsTransactionItem = ({ transaction }: OwnProps) => { + const { getStarsReceipt } = getActions(); + const { + date, + stars, + photo, + isRefund, + peer: transactionPeer, + } = transaction; + const lang = useLang(); + + const peerId = transactionPeer.type === 'peer' ? transactionPeer.id : undefined; + const peer = useSelector(selectOptionalPeer(peerId)); + + const data = useMemo(() => { + let title = transaction.title; + let description; + let avatarPeer: ApiPeer | CustomPeer | undefined; + + if (transaction.peer.type === 'peer') { + description = peer && getSenderTitle(lang, peer); + avatarPeer = peer || CUSTOM_PEER_PREMIUM; + } else { + const customPeer = buildStarsTransactionCustomPeer(transaction.peer); + title = lang(customPeer.titleKey); + description = lang(customPeer.subtitleKey!); + avatarPeer = customPeer; + } + + if (transaction.photo) { + avatarPeer = undefined; + } + + return { + title, + description, + avatarPeer, + }; + }, [lang, peer, transaction]); + + const handleClick = useLastCallback(() => { + getStarsReceipt({ transaction }); + }); + + return ( +
+ +
+

{data.title}

+

{data.description}

+

+ {formatDateTimeToString(date * 1000, lang.code, true)} + {isRefund && ` — (${lang('StarsRefunded')})`} +

+
+
+ + {formatStarsTransactionAmount(stars)} + + +
+
+ ); +}; + +export default memo(StarsTransactionItem); diff --git a/src/components/modals/webApp/WebAppModal.tsx b/src/components/modals/webApp/WebAppModal.tsx index 296624a8a..1c8776899 100644 --- a/src/components/modals/webApp/WebAppModal.tsx +++ b/src/components/modals/webApp/WebAppModal.tsx @@ -32,7 +32,7 @@ import useSyncEffect from '../../../hooks/useSyncEffect'; import usePopupLimit from './hooks/usePopupLimit'; import useWebAppFrame from './hooks/useWebAppFrame'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import ConfirmDialog from '../../ui/ConfirmDialog'; import DropdownMenu from '../../ui/DropdownMenu'; @@ -700,13 +700,14 @@ export default memo(withGlobal( 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: isPaymentModalOpen || isStarPaymentModalOpen, paymentStatus: status, }; }, diff --git a/src/components/payment/Checkout.module.scss b/src/components/payment/Checkout.module.scss index 12831c023..d1dfd3b18 100644 --- a/src/components/payment/Checkout.module.scss +++ b/src/components/payment/Checkout.module.scss @@ -10,6 +10,7 @@ } .checkout-picture { height: 6rem; + border-radius: 0.5rem; @supports (aspect-ratio: 1) { margin: 0.25rem 0.75rem 0 0; diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx index 35eb91254..51058eba3 100644 --- a/src/components/payment/Checkout.tsx +++ b/src/components/payment/Checkout.tsx @@ -257,9 +257,12 @@ function renderCheckoutItem({ onClick?: NoneToVoidFunction; customIcon?: string; }) { + const isMultiline = Boolean(title && label !== title); + return ( = ({ needCardholderName={needCardholderName} needCountry={needCountry} needZip={needZip} - countryList={countryList} + countryList={countryList!} /> ); case PaymentStep.ShippingInfo: @@ -351,7 +353,7 @@ const PaymentModal: FC = ({ needEmail={Boolean(isEmailRequested || shouldSendEmailToProvider)} needPhone={Boolean(isPhoneRequested || shouldSendPhoneToProvider)} needName={Boolean(isNameRequested)} - countryList={countryList} + countryList={countryList!} /> ); case PaymentStep.Shipping: @@ -390,7 +392,7 @@ const PaymentModal: FC = ({ }); }, [sendCredentialsInfo, paymentState]); - const handleButtonClick = useCallback(() => { + const handleButtonClick = useLastCallback(() => { switch (step) { case PaymentStep.ShippingInfo: setIsLoading(true); @@ -462,7 +464,7 @@ const PaymentModal: FC = ({ return; } - if (isShippingAddressRequested && !paymentState.shipping) { + if (isShippingAddressRequested && !paymentState.shipping && shippingOptions?.length) { setStep(PaymentStep.Shipping); return; } @@ -472,11 +474,7 @@ const PaymentModal: FC = ({ break; } } - }, [ - isEmailRequested, isNameRequested, isPhoneRequested, isShippingAddressRequested, nativeProvider, passwordValidUntil, - paymentDispatch, paymentState, requestId, savedInfo, sendCredentials, sendForm, setStep, smartGlocalToken, step, - stripeId, twoFaPassword, validatePaymentPassword, validateRequest, isPaymentFormUrl, - ]); + }); useEffect(() => { return step === PaymentStep.ConfirmPassword @@ -529,7 +527,7 @@ const PaymentModal: FC = ({ switch (step) { case PaymentStep.Checkout: - return !isTosAccepted; + return Boolean(invoice?.termsUrl) && !isTosAccepted; case PaymentStep.PaymentInfo: return Boolean( @@ -648,9 +646,19 @@ export default memo(withGlobal( temporaryPassword, isExtendedMedia, url, - botName, + botId, + type, } = selectTabState(global).payment; + const countryList = global.countryList.general; + + // Handled in `StarPaymentModal` + if (type === 'stars') { + return { + countryList, + }; + } + let providerName = nativeProvider; if (!providerName && url) { providerName = url.startsWith(DONATE_PROVIDER_URL) ? DONATE_PROVIDER : undefined; @@ -669,6 +677,8 @@ export default memo(withGlobal( currency, prices, } = (invoiceContainer || {}); + const bot = botId ? selectUser(global, botId) : undefined; + const botName = getUserFullName(bot); return { step, @@ -694,7 +704,7 @@ export default memo(withGlobal( error, confirmPaymentUrl: confirmPaymentUrl ?? url, isPaymentFormUrl: Boolean(!nativeProvider && url), - countryList: global.countryList.general, + countryList, requestId, hasShippingOptions: Boolean(shippingOptions?.length), smartGlocalToken: smartGlocalCredentials?.token, diff --git a/src/components/payment/ReceiptModal.module.scss b/src/components/payment/ReceiptModal.module.scss new file mode 100644 index 000000000..bab03512a --- /dev/null +++ b/src/components/payment/ReceiptModal.module.scss @@ -0,0 +1,56 @@ +.positive { + color: var(--color-success); +} + +.negative { + color: var(--color-error); +} + +.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: 1.25rem; + font-weight: 500; + line-height: 1.325; +} + +.title, .description, .amount { + margin-bottom: 0; +} + +.tid { + font-family: var(--font-family-monospace); + font-size: 0.875rem; + cursor: pointer; +} + +.description { + text-align: center; +} + +.footer { + text-align: center; + margin-block: 0.5rem; +} + +.starsBackground { + position: absolute; + height: 8rem; + top: -8.5rem; + left: 50%; + transform: translateX(-50%); +} + +.copyIcon { + margin-inline-start: 0.25rem; + color: var(--color-primary); +} diff --git a/src/components/payment/ReceiptModal.tsx b/src/components/payment/ReceiptModal.tsx index a440d3ce3..6d95f4dda 100644 --- a/src/components/payment/ReceiptModal.tsx +++ b/src/components/payment/ReceiptModal.tsx @@ -1,20 +1,34 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo } from '../../lib/teact/teact'; -import { withGlobal } from '../../global'; +import { getActions, withGlobal } from '../../global'; -import type { ApiShippingAddress, ApiWebDocument } from '../../api/types'; -import type { Price } from '../../types'; +import type { + ApiReceipt, ApiReceiptRegular, ApiShippingAddress, + ApiStarsTransactionPeer, + ApiUser, +} from '../../api/types'; -import { selectTabState } from '../../global/selectors'; +import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../global/helpers/payments'; +import { selectTabState, selectUser } from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { copyTextToClipboard } from '../../util/clipboard'; +import { formatDateTimeToString } from '../../util/date/dateFormat'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; +import Icon from '../common/icons/Icon'; +import StarIcon from '../common/icons/StarIcon'; +import SafeLink from '../common/SafeLink'; +import TableInfoModal, { type TableData } from '../modals/common/TableInfoModal'; import Button from '../ui/Button'; import Modal from '../ui/Modal'; import Checkout from './Checkout'; import './PaymentModal.scss'; +import styles from './ReceiptModal.module.scss'; + +import StarsBackground from '../../assets/stars-bg.png'; export type OwnProps = { isOpen?: boolean; @@ -22,38 +36,120 @@ 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?: ApiReceipt; + bot?: ApiUser; }; const ReceiptModal: FC = ({ - isOpen, - onClose, - prices, - shippingPrices, - tipAmount, - totalAmount, - currency, - info, - photo, - text, - title, - credentialsTitle, - shippingMethod, + isOpen, receipt, bot, onClose, }) => { + const { showNotification } = getActions(); + const lang = useLang(); + + const starModalData = useMemo(() => { + if (receipt?.type !== 'stars') return undefined; + + const customPeer = (receipt.peer && receipt.peer.type !== 'peer' && buildStarsTransactionCustomPeer(receipt.peer)) + || undefined; + + const botId = receipt.botId || (receipt.peer?.type === 'peer' ? receipt.peer.id : undefined); + const toName = receipt.peer && lang(getStarsPeerTitleKey(receipt.peer)); + + const title = receipt.title || (customPeer ? lang(customPeer.titleKey) : undefined); + + const header = ( +
+ + {title &&

{title}

} +

{receipt.text}

+

+ + {formatStarsTransactionAmount(receipt.totalAmount)} + + +

+
+ ); + + const tableData = [ + [ + lang(receipt.totalAmount < 0 ? 'Stars.Transaction.To' : 'Stars.Transaction.Via'), + botId ? { chatId: botId } : toName || '', + ], + [lang('Stars.Transaction.Id'), ( + { + copyTextToClipboard(receipt.transactionId); + showNotification({ + message: lang('StarsTransactionIDCopied'), + }); + }} + > + {receipt.transactionId} + + + )], + [lang('Stars.Transaction.Date'), formatDateTimeToString(receipt.date * 1000, lang.code, true)], + ] satisfies TableData; + + const footerText = lang('lng_credits_box_out_about'); + const footerTextParts = footerText.split('{link}'); + + const footer = ( + + {footerTextParts[0]} + + {footerTextParts[1]} + + ); + + return { + header, + tableData, + footer, + avatarPeer: !receipt.photo ? (bot || customPeer) : undefined, + }; + }, [lang, receipt, bot]); + + if (receipt?.type === 'regular') { + return ; + } + + return ( + + ); +}; + +function ReceiptModalRegular({ + isOpen, receipt, onClose, +}: { + isOpen?: boolean; + receipt: ApiReceiptRegular; + onClose: NoneToVoidFunction; +}) { + const { + credentialsTitle, + currency, + prices, + tipAmount, + totalAmount, + info, + photo, + shippingMethod, + shippingPrices, + text, + title, + } = receipt; const lang = useLang(); const [isModalOpen, openModal, closeModal] = useFlag(); @@ -113,37 +209,18 @@ const ReceiptModal: FC = ({ ); -}; +} export default memo(withGlobal( (global): StateProps => { const { receipt } = selectTabState(global).payment; - const { - currency, - prices, - info, - totalAmount, - credentialsTitle, - shippingPrices, - shippingMethod, - photo, - text, - title, - tipAmount, - } = (receipt || {}); + + const botId = receipt?.type === 'stars' && (receipt.botId || (receipt.peer?.type === 'peer' && receipt.peer.id)); + const bot = botId ? selectUser(global, botId) : undefined; return { - currency, - prices, - info, - tipAmount, - totalAmount, - credentialsTitle, - shippingPrices, - shippingMethod, - photo, - text, - title, + receipt, + bot, }; }, )(ReceiptModal)); @@ -171,3 +248,18 @@ function getCheckoutInfo(paymentMethod?: string, shippingMethod, }; } + +function getStarsPeerTitleKey(peer: ApiStarsTransactionPeer) { + switch (peer.type) { + case 'appStore': + return 'AppStore'; + case 'playMarket': + return 'PlayMarket'; + case 'fragment': + return 'Fragment'; + case 'premiumBot': + return 'StarsTransactionBot'; + default: + return 'Stars.Transaction.Unsupported.Title'; + } +} diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index 9b7a4b7b2..c03c96463 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -33,7 +33,7 @@ import { useFolderManagerForChatsCount } from '../../hooks/useFolderManager'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import Button from '../ui/Button'; import ConfirmDialog from '../ui/ConfirmDialog'; import SearchInput from '../ui/SearchInput'; diff --git a/src/components/right/management/ManageBot.tsx b/src/components/right/management/ManageBot.tsx index c13d54a66..dff131d27 100644 --- a/src/components/right/management/ManageBot.tsx +++ b/src/components/right/management/ManageBot.tsx @@ -26,7 +26,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import AvatarEditable from '../../ui/AvatarEditable'; import FloatingActionButton from '../../ui/FloatingActionButton'; import InputText from '../../ui/InputText'; diff --git a/src/components/right/management/ManageGroupMembers.tsx b/src/components/right/management/ManageGroupMembers.tsx index 53da047ff..a4e044987 100644 --- a/src/components/right/management/ManageGroupMembers.tsx +++ b/src/components/right/management/ManageGroupMembers.tsx @@ -22,7 +22,7 @@ import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation' import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import NothingFound from '../../common/NothingFound'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import FloatingActionButton from '../../ui/FloatingActionButton'; diff --git a/src/components/right/statistics/BoostStatistics.tsx b/src/components/right/statistics/BoostStatistics.tsx index 606504d75..59168ae2e 100644 --- a/src/components/right/statistics/BoostStatistics.tsx +++ b/src/components/right/statistics/BoostStatistics.tsx @@ -24,7 +24,7 @@ import { getBoostProgressInfo } from '../../common/helpers/boostInfo'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import LinkField from '../../common/LinkField'; import PremiumProgress from '../../common/PremiumProgress'; import PrivateChatInfo from '../../common/PrivateChatInfo'; @@ -320,7 +320,6 @@ const BoostStatistics = ({ activeKey={activeKey} renderCount={tabs.length} shouldRestoreHeight - className="shared-media-transition" > {renderContent()} diff --git a/src/components/right/statistics/StatisticsRecentMessage.tsx b/src/components/right/statistics/StatisticsRecentMessage.tsx index 4abcc47e7..ba9baf9ea 100644 --- a/src/components/right/statistics/StatisticsRecentMessage.tsx +++ b/src/components/right/statistics/StatisticsRecentMessage.tsx @@ -18,7 +18,7 @@ import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import useLang from '../../../hooks/useLang'; import useMedia from '../../../hooks/useMedia'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import StatisticsRecentPostMeta from './StatisticsRecentPostMeta'; import styles from './StatisticsRecentPost.module.scss'; diff --git a/src/components/right/statistics/StatisticsRecentPostMeta.tsx b/src/components/right/statistics/StatisticsRecentPostMeta.tsx index 0a10b9b68..4c3728575 100644 --- a/src/components/right/statistics/StatisticsRecentPostMeta.tsx +++ b/src/components/right/statistics/StatisticsRecentPostMeta.tsx @@ -6,7 +6,7 @@ import { formatIntegerCompact } from '../../../util/textFormat'; import useLang from '../../../hooks/useLang'; -import Icon from '../../common/Icon'; +import Icon from '../../common/icons/Icon'; import styles from './StatisticsRecentPost.module.scss'; diff --git a/src/components/story/MediaStory.tsx b/src/components/story/MediaStory.tsx index 69f2f5ec5..26e522ec3 100644 --- a/src/components/story/MediaStory.tsx +++ b/src/components/story/MediaStory.tsx @@ -18,7 +18,7 @@ import useLastCallback from '../../hooks/useLastCallback'; import useMedia from '../../hooks/useMedia'; import useMenuPosition from '../../hooks/useMenuPosition'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import Menu from '../ui/Menu'; import MenuItem from '../ui/MenuItem'; import MediaAreaOverlay from './mediaArea/MediaAreaOverlay'; diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index c2a194a37..ee79e7487 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -53,7 +53,7 @@ import useStoryProps from './hooks/useStoryProps'; import Avatar from '../common/Avatar'; import Composer from '../common/Composer'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import Button from '../ui/Button'; import DropdownMenu from '../ui/DropdownMenu'; import MenuItem from '../ui/MenuItem'; diff --git a/src/components/story/StoryCaption.tsx b/src/components/story/StoryCaption.tsx index 14f451a93..21391f738 100644 --- a/src/components/story/StoryCaption.tsx +++ b/src/components/story/StoryCaption.tsx @@ -7,6 +7,7 @@ import type { ApiStory } from '../../api/types'; import { requestForcedReflow, requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom'; import buildClassName from '../../util/buildClassName'; +import calcTextLineHeightAndCount from '../../util/element/calcTextLineHeightAndCount'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useLang from '../../hooks/useLang'; @@ -94,8 +95,8 @@ function StoryCaption({ const textContainer = textRef.current; const textOffsetTop = textContainer.offsetTop; - const lineHeight = parseInt(getComputedStyle(textContainer).lineHeight, 10); - const isOverflowing = textContainer.clientHeight > lineHeight * LINES_TO_SHOW; + const { lineHeight, totalLines } = calcTextLineHeightAndCount(textContainer); + const isOverflowing = totalLines > LINES_TO_SHOW; const overflowShift = textOffsetTop + lineHeight * LINES_TO_SHOW; return () => { diff --git a/src/components/story/StoryFooter.tsx b/src/components/story/StoryFooter.tsx index 4a35ccd41..ec06a3b23 100644 --- a/src/components/story/StoryFooter.tsx +++ b/src/components/story/StoryFooter.tsx @@ -11,7 +11,7 @@ import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import AvatarList from '../common/AvatarList'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import ReactionAnimatedEmoji from '../common/reactions/ReactionAnimatedEmoji'; import Button from '../ui/Button'; diff --git a/src/components/ui/Button.scss b/src/components/ui/Button.scss index f733b0f3c..334cabd07 100644 --- a/src/components/ui/Button.scss +++ b/src/components/ui/Button.scss @@ -38,7 +38,6 @@ color: white; line-height: 1.2; cursor: var(--custom-cursor, pointer); - text-transform: uppercase; flex-shrink: 0; position: relative; overflow: hidden; diff --git a/src/components/ui/ListItem.tsx b/src/components/ui/ListItem.tsx index e1202c283..4c0cb3b18 100644 --- a/src/components/ui/ListItem.tsx +++ b/src/components/ui/ListItem.tsx @@ -16,7 +16,7 @@ import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useMenuPosition from '../../hooks/useMenuPosition'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import Button from './Button'; import Menu from './Menu'; import MenuItem from './MenuItem'; diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index 3e4e8888c..029a3f58b 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -211,4 +211,10 @@ .dialog-button-spacer { flex-grow: 1; } + + .modal-absolute-close-button { + position: absolute; + top: 0.5rem; + left: 0.5rem; + } } diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 9a5943c47..d36acd49d 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -16,6 +16,7 @@ import useLastCallback from '../../hooks/useLastCallback'; import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps'; import useShowTransition from '../../hooks/useShowTransition'; +import Icon from '../common/icons/Icon'; import Button from './Button'; import Portal from './Portal'; @@ -31,6 +32,7 @@ type OwnProps = { header?: TeactNode; isSlim?: boolean; hasCloseButton?: boolean; + hasAbsoluteCloseButton?: boolean; noBackdrop?: boolean; noBackdropClose?: boolean; children: React.ReactNode; @@ -54,6 +56,7 @@ const Modal: FC = ({ isSlim, header, hasCloseButton, + hasAbsoluteCloseButton, noBackdrop, noBackdropClose, children, @@ -71,6 +74,7 @@ const Modal: FC = ({ ); // eslint-disable-next-line no-null/no-null const modalRef = useRef(null); + const withCloseButton = hasCloseButton || hasAbsoluteCloseButton; useEffect(() => { if (!isOpen) { @@ -125,21 +129,20 @@ const Modal: FC = ({ return header; } - if (!title) { - return undefined; - } + if (!title && !withCloseButton) return undefined; return (
- {hasCloseButton && ( + {withCloseButton && ( )}
{title}
diff --git a/src/components/ui/ProgressSpinner.tsx b/src/components/ui/ProgressSpinner.tsx index e05bdb267..fea2a3b58 100644 --- a/src/components/ui/ProgressSpinner.tsx +++ b/src/components/ui/ProgressSpinner.tsx @@ -9,7 +9,7 @@ import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListene import { useStateRef } from '../../hooks/useStateRef'; import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio'; -import Icon from '../common/Icon'; +import Icon from '../common/icons/Icon'; import './ProgressSpinner.scss'; diff --git a/src/components/ui/TabList.tsx b/src/components/ui/TabList.tsx index cfca24df0..f44fb3609 100644 --- a/src/components/ui/TabList.tsx +++ b/src/components/ui/TabList.tsx @@ -5,6 +5,7 @@ import type { MenuItemContextAction } from './ListItem'; import { ALL_FOLDER_ID } from '../../config'; import animateHorizontalScroll from '../../util/animateHorizontalScroll'; +import buildClassName from '../../util/buildClassName'; import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment'; import useHorizontalScroll from '../../hooks/useHorizontalScroll'; @@ -29,6 +30,7 @@ type OwnProps = { areFolders?: boolean; activeTab: number; big?: boolean; + className?: string; onSwitchTab: (index: number) => void; contextRootElementSelector?: string; }; @@ -39,7 +41,7 @@ const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300; const TabList: FC = ({ tabs, areFolders, activeTab, big, onSwitchTab, - contextRootElementSelector, + contextRootElementSelector, className, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); @@ -75,7 +77,7 @@ const TabList: FC = ({ return (
diff --git a/src/config.ts b/src/config.ts index 51f1b872c..d153d4bcb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -49,7 +49,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-v35'; +export const LANG_CACHE_NAME = 'tt-lang-packs-v37'; 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/bots.ts b/src/global/actions/api/bots.ts index d63de7446..e7e74f29b 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -95,7 +95,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur } const { receiptMessageId } = button; actions.getReceipt({ - receiptMessageId, chatId: chat.id, messageId, tabId, + chatId: chat.id, messageId: receiptMessageId, tabId, }); break; } diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 73e9b369d..432ad0d08 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -239,6 +239,7 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => { actions.requestChatUpdate({ chatId: id }); } actions.closeStoryViewer({ tabId }); + actions.closeStarsBalanceModal({ tabId }); }); addActionHandler('openSavedDialog', (global, actions, payload): ActionReturnType => { diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index bc97eb609..346bff8a2 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -2043,6 +2043,29 @@ addActionHandler('loadMessageViews', async (global, actions, payload): Promise => { + const { chatId, ids } = payload; + + const chat = selectChat(global, chatId); + if (!chat) return; + + const result = await callApi('fetchFactChecks', { + chat, + ids, + }); + + if (!result) return; + + global = getGlobal(); + result.forEach((factCheck, i) => { + global = updateChatMessage(global, chatId, ids[i], { + factCheck, + }); + }); + + setGlobal(global); +}); + addActionHandler('loadOutboxReadDate', async (global, actions, payload): Promise => { const { chatId, messageId } = payload; diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 155db88ac..977763c6d 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -1,4 +1,4 @@ -import type { ApiInvoice, ApiRequestInputInvoice } from '../../../api/types'; +import type { ApiInputInvoiceStars, ApiRequestInputInvoice } from '../../../api/types'; import type { ApiCredentials } from '../../../components/payment/PaymentModal'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import { PaymentStep } from '../../../types'; @@ -15,7 +15,7 @@ import { getRequestInputInvoice } from '../../helpers/payments'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addChats, - addUsers, closeInvoice, + addUsers, appendStarsTransactions, closeInvoice, setInvoiceInfo, setPaymentForm, setPaymentStep, setReceipt, @@ -23,13 +23,14 @@ import { setSmartGlocalCardInfo, setStripeCardInfo, updateChatFullInfo, updatePayment, + updateReceiptFromStarsTransaction, updateShippingOptions, + updateStarsBalance, } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { selectChat, selectChatFullInfo, - selectChatMessage, selectPaymentFormId, selectPaymentInputInvoice, selectPaymentRequestId, selectProviderPublicToken, @@ -37,7 +38,6 @@ import { selectSmartGlocalCredentials, selectStripeCredentials, selectTabState, - selectUser, } from '../../selectors'; const LOCAL_BOOST_COOLDOWN = 86400; // 24 hours @@ -66,72 +66,79 @@ addActionHandler('openInvoice', async (global, actions, payload): Promise return; } - const invoice = await getPaymentForm(global, requestInputInvoice, tabId); + const result = await getPaymentForm(global, requestInputInvoice, tabId); - if (!invoice) { + if (!result) { return; } + const { form, invoice } = result; + global = getGlobal(); + global = setInvoiceInfo(global, invoice, tabId); - global = updateTabState(global, { - payment: { - ...selectTabState(global, tabId).payment, - inputInvoice: payload, - isPaymentModalOpen: true, - status: 'cancelled', - isExtendedMedia: (payload as any).isExtendedMedia, - }, + global = updatePayment(global, { + inputInvoice: payload, + isPaymentModalOpen: form.type === 'regular', + isExtendedMedia: (payload as any).isExtendedMedia, + status: undefined, }, tabId); + if (form.type === 'stars') { + global = updateTabState(global, { + isStarPaymentModalOpen: true, + }, tabId); + } setGlobal(global); }); async function getPaymentForm( global: T, inputInvoice: ApiRequestInputInvoice, ...[tabId = getCurrentTabId()]: TabArgs -): Promise { +) { const result = await callApi('getPaymentForm', inputInvoice); if (!result) { return undefined; } const { - form, invoice, users, botId, + form, invoice, users, } = result; global = getGlobal(); + global = addUsers(global, buildCollectionByKey(users, 'id')); global = setPaymentForm(global, form, tabId); global = setPaymentStep(global, PaymentStep.Checkout, tabId); - global = updatePayment(global, { - botName: selectUser(global, botId)?.firstName, - }, tabId); setGlobal(global); - return invoice; + return { form, invoice }; } addActionHandler('getReceipt', async (global, actions, payload): Promise => { const { - receiptMessageId, chatId, messageId, tabId = getCurrentTabId(), + chatId, messageId, tabId = getCurrentTabId(), } = payload; const chat = chatId && selectChat(global, chatId); - if (!messageId || !receiptMessageId || !chat) { + if (!messageId || !chat) { return; } - const result = await callApi('getReceipt', chat, receiptMessageId); + const result = await callApi('getReceipt', chat, messageId); if (!result) { return; } global = getGlobal(); - const message = selectChatMessage(global, chat.id, messageId); global = addUsers(global, buildCollectionByKey(result.users, 'id')); - global = setReceipt(global, result.receipt, message, tabId); + global = setReceipt(global, result.receipt, tabId); setGlobal(global); }); +addActionHandler('getStarsReceipt', (global, actions, payload): ActionReturnType => { + const { transaction, tabId = getCurrentTabId() } = payload; + return updateReceiptFromStarsTransaction(global, transaction, tabId); +}); + addActionHandler('clearPaymentError', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; global = updateTabState(global, { @@ -220,6 +227,52 @@ addActionHandler('sendPaymentForm', async (global, actions, payload): Promise => { + 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 requestInputInvoice = getRequestInputInvoice(global, inputInvoice); + if (!requestInputInvoice) { + return; + } + + const result = await callApi('sendStarPaymentForm', { + inputInvoice: requestInputInvoice, + formId, + }); + + if (!result) { + return; + } + + global = getGlobal(); + global = updatePayment(global, { status: 'paid' }, tabId); + global = closeInvoice(global, tabId); + setGlobal(global); + + actions.apiUpdate({ + '@type': 'updatePaymentStateCompleted', + inputInvoice, + }); + actions.loadStarStatus(); }); async function sendStripeCredentials( @@ -284,9 +337,20 @@ async function sendSmartGlocalCredentials( security_code: data.cvv.replace(/\D+/g, ''), }, }; - const url = DEBUG_PAYMENT_SMART_GLOCAL - ? 'https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card' - : 'https://tgb.smart-glocal.com/cds/v1/tokenize/card'; + + const tokenizeUrl = selectTabState(global, tabId).payment.nativeParams?.tokenizeUrl; + + let url; + if (DEBUG_PAYMENT_SMART_GLOCAL) { + url = 'https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card'; + } else { + url = 'https://tgb.smart-glocal.com/cds/v1/tokenize/card'; + } + + if (tokenizeUrl?.startsWith('https://') + && tokenizeUrl.endsWith('.smart-glocal.com/cds/v1/tokenize/card')) { + url = tokenizeUrl; + } const response = await fetch(url, { method: 'POST', @@ -507,7 +571,7 @@ async function validateRequestedInfo( global = getGlobal(); global = setRequestInfoId(global, id, tabId); - if (shippingOptions) { + if (shippingOptions?.length) { global = updateShippingOptions(global, shippingOptions, tabId); global = setPaymentStep(global, PaymentStep.Shipping, tabId); } else { @@ -806,7 +870,7 @@ addActionHandler('applyGiftCode', async (global, actions, payload): Promise => { + 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 = addChats(global, buildCollectionByKey(status.chats, 'id')); + global = addUsers(global, buildCollectionByKey(status.users, 'id')); + + global = { + ...global, + stars: { + ...currentStatus, + balance: status.balance, + topupOptions: topupOptions || currentStatus!.topupOptions, + history: { + all: undefined, + inbound: undefined, + outbound: undefined, + }, + }, + }; + global = appendStarsTransactions(global, 'all', status.history, status.nextOffset); + setGlobal(global); +}); + +addActionHandler('loadStarsTransactions', async (global, actions, payload): Promise => { + 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 = addChats(global, buildCollectionByKey(result.chats, 'id')); + global = addUsers(global, buildCollectionByKey(result.users, 'id')); + + global = updateStarsBalance(global, result.balance); + global = appendStarsTransactions(global, type, result.history, result.nextOffset); + setGlobal(global); +}); diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index 6d6749b3e..298d412df 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -3,35 +3,32 @@ import type { ActionReturnType } from '../../types'; import { areDeepEqual } from '../../../util/areDeepEqual'; import { formatCurrency } from '../../../util/formatCurrency'; import * as langProvider from '../../../util/langProvider'; -import { IS_PRODUCTION_HOST } from '../../../util/windowEnvironment'; import { addActionHandler, setGlobal } from '../../index'; -import { closeInvoice } from '../../reducers'; +import { closeInvoice, updateStarsBalance } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; -import { selectChatMessage, selectTabState } from '../../selectors'; +import { selectTabState } from '../../selectors'; addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { switch (update['@type']) { case 'updatePaymentStateCompleted': { Object.values(global.byTabId).forEach(({ id: tabId }) => { - const { inputInvoice } = selectTabState(global, tabId).payment; + const { inputInvoice, invoice } = selectTabState(global, tabId).payment; - if (inputInvoice && 'chatId' in inputInvoice && 'messageId' in inputInvoice) { - const message = selectChatMessage(global, inputInvoice.chatId, inputInvoice.messageId); + if (!areDeepEqual(inputInvoice, update.inputInvoice)) return; - if (message && message.content.invoice) { - const { amount, currency, title } = message.content.invoice; + if (invoice) { + const { amount, currency, title } = invoice; - actions.showNotification({ - tabId, - message: langProvider.translate('PaymentInfoHint', [ - formatCurrency(amount, currency, langProvider.getTranslationFn().code), - title, - ]), - }); - } + actions.showNotification({ + tabId, + message: langProvider.translate('PaymentInfoHint', [ + formatCurrency(amount, currency, langProvider.getTranslationFn().code), + title, + ]), + }); } - if (inputInvoice && inputInvoice.type === 'giftcode') { + if (inputInvoice?.type === 'giftcode') { if (!inputInvoice.userIds) { return; } @@ -46,30 +43,27 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { }, }, tabId); global = closeInvoice(global, tabId); - setGlobal(global); } } - // On the production host, the payment frame receives a message with the payment event, - // after which the payment form closes. In other cases, the payment form must be closed manually. - // Closing the invoice will cause the closing of the Payment Modal dialog and then closing the payment. - if (!IS_PRODUCTION_HOST) { - global = closeInvoice(global, tabId); - } - - if (update.slug && inputInvoice && 'slug' in inputInvoice && inputInvoice.slug !== update.slug) { - return; - } - - global = updateTabState(global, { - payment: { - ...selectTabState(global, tabId).payment, - status: 'paid', - }, - }, tabId); + setGlobal(global); }); + + break; + } + + case 'updateStarsBalance': { + const stars = global.stars; + if (!stars) { + return; + } + + global = updateStarsBalance(global, update.balance); + + setGlobal(global); + + actions.loadStarStatus(); + break; } } - - return undefined; }); diff --git a/src/global/actions/ui/payments.ts b/src/global/actions/ui/payments.ts index bbf212552..3b19a1b98 100644 --- a/src/global/actions/ui/payments.ts +++ b/src/global/actions/ui/payments.ts @@ -2,13 +2,15 @@ import type { ActionReturnType } from '../../types'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { addActionHandler } from '../../index'; -import { clearPayment, closeInvoice } from '../../reducers'; +import { clearPayment, closeInvoice, updatePayment } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { selectTabState } from '../../selectors'; addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; - const status = selectTabState(global, tabId).payment.status; + const payment = selectTabState(global, tabId).payment; + const status = payment.status || 'cancelled'; + const originPayment = selectTabState(global, tabId).starsBalanceModal?.originPayment; global = clearPayment(global, tabId); global = closeInvoice(global, tabId); global = updateTabState(global, { @@ -16,7 +18,18 @@ addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnTy ...selectTabState(global, tabId).payment, status, }, + ...(originPayment && { + starsBalanceModal: undefined, + }), }, tabId); + + // Re-open previous payment modal + if (originPayment) { + global = updatePayment(global, originPayment, tabId); + global = updateTabState(global, { + isStarPaymentModalOpen: true, + }, tabId); + } return global; }); @@ -39,3 +52,23 @@ addActionHandler('closeGiftCodeModal', (global, actions, payload): ActionReturnT giftCodeModal: undefined, }, tabId); }); + +addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionReturnType => { + const { originPayment, tabId = getCurrentTabId() } = payload || {}; + + global = clearPayment(global, tabId); + + return updateTabState(global, { + starsBalanceModal: { + originPayment, + }, + }, tabId); +}); + +addActionHandler('closeStarsBalanceModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + starsBalanceModal: undefined, + }, tabId); +}); diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index fb2077a90..27e6fbc94 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -1,12 +1,16 @@ -import type { ApiInputInvoice, ApiRequestInputInvoice } from '../../api/types'; +import type { + ApiInputInvoice, ApiRequestInputInvoice, ApiStarsTransactionPeer, ApiStarsTransactionPeerPeer, +} from '../../api/types'; +import type { CustomPeer } from '../../types'; import type { GlobalState } from '../types'; +import { formatInteger } from '../../util/textFormat'; import { selectChat, selectUser } from '../selectors'; export function getRequestInputInvoice( global: T, inputInvoice: ApiInputInvoice, ): ApiRequestInputInvoice | undefined { - if (inputInvoice.type === 'slug') return inputInvoice; + if (inputInvoice.type === 'slug' || inputInvoice.type === 'stars') return inputInvoice; if (inputInvoice.type === 'message') { const chat = selectChat(global, inputInvoice.chatId); @@ -71,3 +75,64 @@ export function getRequestInputInvoice( return undefined; } + +export function buildStarsTransactionCustomPeer( + peer: Exclude, +): CustomPeer { + if (peer.type === 'appStore') { + return { + avatarIcon: 'star', + isCustomPeer: true, + titleKey: 'Stars.Intro.Transaction.AppleTopUp.Title', + subtitleKey: 'Stars.Intro.Transaction.AppleTopUp.Subtitle', + peerColorId: 5, + }; + } + + if (peer.type === 'playMarket') { + return { + avatarIcon: 'star', + isCustomPeer: true, + titleKey: 'Stars.Intro.Transaction.GoogleTopUp.Title', + subtitleKey: 'Stars.Intro.Transaction.GoogleTopUp.Subtitle', + peerColorId: 3, + }; + } + + if (peer.type === 'fragment') { + return { + avatarIcon: 'star', + isCustomPeer: true, + titleKey: 'Stars.Intro.Transaction.FragmentTopUp.Title', + subtitleKey: 'Stars.Intro.Transaction.FragmentTopUp.Subtitle', + peerColorId: -1, // Defaults to black + }; + } + + if (peer.type === 'premiumBot') { + return { + avatarIcon: 'star', + isCustomPeer: true, + titleKey: 'Stars.Intro.Transaction.PremiumBotTopUp.Title', + subtitleKey: 'Stars.Intro.Transaction.PremiumBotTopUp.Subtitle', + peerColorId: 1, + withPremiumGradient: true, + }; + } + + return { + avatarIcon: 'star', + isCustomPeer: true, + titleKey: 'Stars.Intro.Transaction.Unsupported.Title', + subtitleKey: 'Stars.Intro.Transaction.Unsupported.Title', + peerColorId: 0, + }; +} + +export function formatStarsTransactionAmount(amount: number) { + if (amount < 0) { + return `- ${formatInteger(Math.abs(amount))}`; + } + + return `+ ${formatInteger(amount)}`; +} diff --git a/src/global/reducers/payments.ts b/src/global/reducers/payments.ts index 45dfdb473..6cb1eff56 100644 --- a/src/global/reducers/payments.ts +++ b/src/global/reducers/payments.ts @@ -1,8 +1,12 @@ import type { - ApiInvoice, ApiMessage, ApiPaymentForm, ApiReceipt, + ApiInvoice, ApiPaymentForm, ApiReceipt, + ApiReceiptStars, + ApiStarsTransaction, } from '../../api/types'; import type { PaymentStep, ShippingOption } from '../../types'; -import type { GlobalState, TabArgs, TabState } from '../types'; +import type { + GlobalState, StarsTransactionType, TabArgs, TabState, +} from '../types'; import { getCurrentTabId } from '../../util/establishMultitabRole'; import { selectTabState } from '../selectors'; @@ -108,25 +112,14 @@ export function setConfirmPaymentUrl( export function setReceipt( global: T, receipt?: ApiReceipt, - message?: ApiMessage, ...[tabId = getCurrentTabId()]: TabArgs ): T { - if (!receipt || !message) { + if (!receipt) { return updatePayment(global, { receipt: undefined }, tabId); } - const { invoice: messageInvoice } = message.content; - const { - photo, text, title, - } = (messageInvoice || {}); - return updatePayment(global, { - receipt: { - ...receipt, - photo, - text, - title, - }, + receipt, }, tabId); } @@ -136,6 +129,7 @@ export function clearPayment( ): T { return updateTabState(global, { payment: {}, + isStarPaymentModalOpen: undefined, }, tabId); } @@ -143,5 +137,68 @@ export function closeInvoice( global: T, ...[tabId = getCurrentTabId()]: TabArgs ): T { - return updatePayment(global, { isPaymentModalOpen: undefined, isExtendedMedia: undefined }, tabId); + global = updatePayment(global, { + isPaymentModalOpen: undefined, + isExtendedMedia: undefined, + }, tabId); + global = updateTabState(global, { isStarPaymentModalOpen: undefined }, tabId); + return global; +} + +export function updateStarsBalance( + global: T, balance: number, +): T { + return { + ...global, + stars: { + ...global.stars, + balance, + }, + }; +} + +export function appendStarsTransactions( + global: T, + type: StarsTransactionType, + transactions: ApiStarsTransaction[], + nextOffset?: string, +): T { + const history = global.stars?.history; + if (!history) { + return global; + } + + const newTypeObject = { + transactions: (history[type]?.transactions || []).concat(transactions), + nextOffset, + }; + + return { + ...global, + stars: { + ...global.stars, + history: { + ...history, + [type]: newTypeObject, + }, + }, + }; +} + +export function updateReceiptFromStarsTransaction( + global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs +): T { + const receipt: ApiReceiptStars = { + type: 'stars', + totalAmount: transaction.stars, + currency: 'XTR', + peer: transaction.peer, + date: transaction.date, + text: transaction.description, + title: transaction.title, + transactionId: transaction.id, + photo: transaction.photo, + }; + + return updatePayment(global, { receipt }, tabId); } diff --git a/src/global/types.ts b/src/global/types.ts index 6b6a689e8..1e8da6dc3 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -60,6 +60,8 @@ import type { ApiSession, ApiSessionData, ApiSponsoredMessage, + ApiStarsTransaction, + ApiStarTopupOption, ApiStealthMode, ApiSticker, ApiStickerSet, @@ -151,6 +153,12 @@ export type IDimensions = { export type ApiPaymentStatus = 'paid' | 'failed' | 'pending' | 'cancelled'; +export type StarsTransactionType = 'all' | 'inbound' | 'outbound'; +export type StarsTransactionHistory = Record; + export type ConfettiStyle = 'poppers' | 'top-down'; export interface TabThread { @@ -225,6 +233,16 @@ export type ChatRequestedTranslations = { manualMessages?: Record; }; +type ConfettiParams = OptionalCombine<{ + style?: ConfettiStyle; + withStars?: boolean; +}, { + top?: number; + left?: number; + width?: number; + height?: number; +}>; + export type TabState = { id: number; isBlurred?: boolean; @@ -477,6 +495,7 @@ export type TabState = { }; payment: { + type?: 'regular' | 'stars'; inputInvoice?: ApiInputInvoice; step?: PaymentStep; status?: ApiPaymentStatus; @@ -514,7 +533,7 @@ export type TabState = { validUntil: number; }; url?: string; - botName?: string; + botId?: string; }; chatCreation?: { @@ -627,6 +646,7 @@ export type TabState = { width?: number; height?: number; style?: ConfettiStyle; + withStars?: boolean; }; urlAuth?: { @@ -752,6 +772,11 @@ export type TabState = { type: 'phone' | 'username'; collectible: string; }; + + starsBalanceModal?: { + originPayment?: TabState['payment']; + }; + isStarPaymentModalOpen?: true; }; export type GlobalState = { @@ -1081,6 +1106,12 @@ export type GlobalState = { byKey: Record; hash: string; }; + + stars?: { + topupOptions: ApiStarTopupOption[]; + balance: number; + history: StarsTransactionHistory; + }; }; export type CallSound = ( @@ -1653,11 +1684,14 @@ export interface ActionPayloads { savedCredentialId?: string; tipAmount?: number; } & WithTabId; + sendStarPaymentForm: WithTabId | undefined; getReceipt: { - receiptMessageId: number; chatId: string; messageId: number; } & WithTabId; + getStarsReceipt: { + transaction: ApiStarsTransaction; + } & WithTabId; sendCredentialsInfo: { credentials: ApiCredentials; } & WithTabId; @@ -2088,6 +2122,15 @@ export interface ActionPayloads { }; } & WithTabId; + loadStarStatus: undefined; + loadStarsTransactions: { + type: StarsTransactionType; + }; + openStarsBalanceModal: { + originPayment?: TabState['payment']; + } & WithTabId; + closeStarsBalanceModal: WithTabId | undefined; + checkChatlistInvite: { slug: string; } & WithTabId; @@ -2150,6 +2193,10 @@ export interface ActionPayloads { ids: number[]; shouldIncrement?: boolean; }; + loadFactChecks: { + chatId: string; + ids: number[]; + }; loadOutboxReadDate: { chatId: string; messageId: number; @@ -2850,13 +2897,7 @@ export interface ActionPayloads { isQuiz?: boolean; } & WithTabId) | undefined; closePollModal: WithTabId | undefined; - requestConfetti: ({ - top: number; - left: number; - width: number; - height: number; - style?: ConfettiStyle; - } & WithTabId) | WithTabId; + requestConfetti: (ConfettiParams & WithTabId) | WithTabId; updateAttachmentSettings: { shouldCompress?: boolean; diff --git a/src/hooks/element/useCollapsibleLines.ts b/src/hooks/element/useCollapsibleLines.ts new file mode 100644 index 000000000..7b1b75145 --- /dev/null +++ b/src/hooks/element/useCollapsibleLines.ts @@ -0,0 +1,89 @@ +import type { RefObject } from 'react'; +import { + useEffect, useLayoutEffect, useRef, useState, +} from '../../lib/teact/teact'; + +import { requestForcedReflow, requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom'; +import calcTextLineHeightAndCount from '../../util/element/calcTextLineHeightAndCount'; +import useDebouncedCallback from '../useDebouncedCallback'; +import useLastCallback from '../useLastCallback'; +import useWindowSize from '../window/useWindowSize'; + +const WINDOW_RESIZE_LINE_RECALC_DEBOUNCE = 200; + +export default function useCollapsibleLines( + ref: RefObject, + maxLinesBeforeCollapse: number, + cutoutRef?: RefObject, + isDisabled?: boolean, +) { + const isFirstRenderRef = useRef(true); + const cutoutHeightRef = useRef(); + const [isCollapsible, setIsCollapsible] = useState(!isDisabled); + const [isCollapsed, setIsCollapsed] = useState(isCollapsible); + + useLayoutEffect(() => { + const element = (cutoutRef || ref).current; + if (isDisabled || !element) return; + + requestMutation(() => { + element.style.maxHeight = isCollapsed ? `${cutoutHeightRef.current}px` : ''; + }); + }, [cutoutRef, isCollapsed, isDisabled, ref]); + + const recalculateTextLines = useLastCallback(() => { + if (isDisabled || !ref.current) { + return; + } + const element = ref.current; + + const { lineHeight, totalLines } = calcTextLineHeightAndCount(element); + if (totalLines > maxLinesBeforeCollapse) { + cutoutHeightRef.current = lineHeight * maxLinesBeforeCollapse; + setIsCollapsible(true); + } else { + setIsCollapsible(false); + setIsCollapsed(false); + } + }); + + const debouncedRecalcTextLines = useDebouncedCallback( + () => requestMeasure(recalculateTextLines), + [recalculateTextLines], + WINDOW_RESIZE_LINE_RECALC_DEBOUNCE, + ); + + useLayoutEffect(() => { + if (!isDisabled && isFirstRenderRef.current) { + requestForcedReflow(() => { + recalculateTextLines(); + + return () => { + isFirstRenderRef.current = false; + const element = (cutoutRef || ref).current; + if (!element) return; + element.style.maxHeight = cutoutHeightRef.current ? `${cutoutHeightRef.current}px` : ''; + }; + }); + } + }, [cutoutRef, isDisabled, recalculateTextLines, ref]); + + // Parent resize is triggered on every collapse/expand, so we do recalculation only on window resize to save resources + const { width: windowWidth } = useWindowSize(); + useEffect(() => { + if (!isDisabled) { + if (isFirstRenderRef.current) return; + + debouncedRecalcTextLines(); + } else { + setIsCollapsible(false); + setIsCollapsed(false); + } + }, [debouncedRecalcTextLines, isDisabled, windowWidth]); + + return { + isCollapsed, + isCollapsible, + setIsCollapsed, + }; +} diff --git a/src/hooks/useFocusAfterAnimation.tsx b/src/hooks/useFocusAfterAnimation.tsx index 8954024e1..fc9a2afe9 100644 --- a/src/hooks/useFocusAfterAnimation.tsx +++ b/src/hooks/useFocusAfterAnimation.tsx @@ -1,10 +1,10 @@ import type { RefObject } from 'react'; import { useEffect } from '../lib/teact/teact'; -import { requestMutation } from '../lib/fasterdom/fasterdom'; +import { requestMeasure } from '../lib/fasterdom/fasterdom'; import { IS_TOUCH_ENV } from '../util/windowEnvironment'; -const DEFAULT_DURATION = 400; +const DEFAULT_DURATION = 300; export default function useFocusAfterAnimation( ref: RefObject, animationDuration = DEFAULT_DURATION, @@ -15,7 +15,7 @@ export default function useFocusAfterAnimation( } setTimeout(() => { - requestMutation(() => { + requestMeasure(() => { ref.current?.focus(); }); }, animationDuration); diff --git a/src/lib/gramjs/client/TelegramClient.js b/src/lib/gramjs/client/TelegramClient.js index 8ae2918dd..82bef7234 100644 --- a/src/lib/gramjs/client/TelegramClient.js +++ b/src/lib/gramjs/client/TelegramClient.js @@ -31,7 +31,7 @@ const RequestState = require('../network/RequestState'); const Deferred = require('../../../util/Deferred').default; const DEFAULT_DC_ID = 2; -const WEBDOCUMENT_DC_ID = 4; +const DEFAULT_WEBDOCUMENT_DC_ID = 4; const EXPORTED_SENDER_RECONNECT_TIMEOUT = 1000; // 1 sec const EXPORTED_SENDER_RELEASE_TIMEOUT = 30000; // 30 sec const WEBDOCUMENT_REQUEST_PART_SIZE = 131072; // 128kb @@ -906,7 +906,10 @@ class TelegramClient { offset, limit: WEBDOCUMENT_REQUEST_PART_SIZE, }); - const sender = await this._borrowExportedSender(WEBDOCUMENT_DC_ID); + + const sender = await this._borrowExportedSender( + this._config?.webfileDcId || DEFAULT_WEBDOCUMENT_DC_ID, + ); const res = await sender.send(downloaded); this.releaseExportedSender(sender); offset += 131072; @@ -953,7 +956,7 @@ class TelegramClient { offset, limit: WEBDOCUMENT_REQUEST_PART_SIZE, }); - const sender = await this._borrowExportedSender(WEBDOCUMENT_DC_ID); + const sender = await this._borrowExportedSender(DEFAULT_WEBDOCUMENT_DC_ID); const res = await sender.send(downloaded); this.releaseExportedSender(sender); offset += 131072; @@ -1099,11 +1102,19 @@ class TelegramClient { return undefined; } + async loadConfig() { + if (!this._config) { + this._config = await this.invoke(new requests.help.GetConfig()); + } + } + async start(authParams) { if (!this.isConnected()) { await this.connect(); } + this.loadConfig(); + if (await checkAuthorization(this, authParams.shouldThrowIfUnauthorized)) { return; } diff --git a/src/lib/gramjs/client/auth.ts b/src/lib/gramjs/client/auth.ts index 7c9e9f4e6..2846876ba 100644 --- a/src/lib/gramjs/client/auth.ts +++ b/src/lib/gramjs/client/auth.ts @@ -1,8 +1,10 @@ -import Api from '../tl/api'; import type TelegramClient from './TelegramClient'; -import utils from '../Utils'; + +import Api from '../tl/api'; + import { sleep } from '../Helpers'; import { computeCheck as computePasswordSrpCheck } from '../Password'; +import utils from '../Utils'; export interface UserAuthParams { phoneNumber: string | (() => Promise); diff --git a/src/lib/gramjs/tl/AllTLObjects.js b/src/lib/gramjs/tl/AllTLObjects.js index cada35099..2bec8de37 100644 --- a/src/lib/gramjs/tl/AllTLObjects.js +++ b/src/lib/gramjs/tl/AllTLObjects.js @@ -1,6 +1,6 @@ const api = require('./api'); -const LAYER = 179; +const LAYER = 181; const tlobjects = {}; for (const tl of Object.values(api)) { diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 3c0c11fe7..b2431e3e1 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -86,7 +86,7 @@ namespace Api { export type TypeImportedContact = ImportedContact; export type TypeContactStatus = ContactStatus; export type TypeMessagesFilter = InputMessagesFilterEmpty | InputMessagesFilterPhotos | InputMessagesFilterVideo | InputMessagesFilterPhotoVideo | InputMessagesFilterDocument | InputMessagesFilterUrl | InputMessagesFilterGif | InputMessagesFilterVoice | InputMessagesFilterMusic | InputMessagesFilterChatPhotos | InputMessagesFilterPhoneCalls | InputMessagesFilterRoundVoice | InputMessagesFilterRoundVideo | InputMessagesFilterMyMentions | InputMessagesFilterGeo | InputMessagesFilterContacts | InputMessagesFilterPinned; - export type TypeUpdate = UpdateNewMessage | UpdateMessageID | UpdateDeleteMessages | UpdateUserTyping | UpdateChatUserTyping | UpdateChatParticipants | UpdateUserStatus | UpdateUserName | UpdateNewAuthorization | UpdateNewEncryptedMessage | UpdateEncryptedChatTyping | UpdateEncryption | UpdateEncryptedMessagesRead | UpdateChatParticipantAdd | UpdateChatParticipantDelete | UpdateDcOptions | UpdateNotifySettings | UpdateServiceNotification | UpdatePrivacy | UpdateUserPhone | UpdateReadHistoryInbox | UpdateReadHistoryOutbox | UpdateWebPage | UpdateReadMessagesContents | UpdateChannelTooLong | UpdateChannel | UpdateNewChannelMessage | UpdateReadChannelInbox | UpdateDeleteChannelMessages | UpdateChannelMessageViews | UpdateChatParticipantAdmin | UpdateNewStickerSet | UpdateStickerSetsOrder | UpdateStickerSets | UpdateSavedGifs | UpdateBotInlineQuery | UpdateBotInlineSend | UpdateEditChannelMessage | UpdateBotCallbackQuery | UpdateEditMessage | UpdateInlineBotCallbackQuery | UpdateReadChannelOutbox | UpdateDraftMessage | UpdateReadFeaturedStickers | UpdateRecentStickers | UpdateConfig | UpdatePtsChanged | UpdateChannelWebPage | UpdateDialogPinned | UpdatePinnedDialogs | UpdateBotWebhookJSON | UpdateBotWebhookJSONQuery | UpdateBotShippingQuery | UpdateBotPrecheckoutQuery | UpdatePhoneCall | UpdateLangPackTooLong | UpdateLangPack | UpdateFavedStickers | UpdateChannelReadMessagesContents | UpdateContactsReset | UpdateChannelAvailableMessages | UpdateDialogUnreadMark | UpdateMessagePoll | UpdateChatDefaultBannedRights | UpdateFolderPeers | UpdatePeerSettings | UpdatePeerLocated | UpdateNewScheduledMessage | UpdateDeleteScheduledMessages | UpdateTheme | UpdateGeoLiveViewed | UpdateLoginToken | UpdateMessagePollVote | UpdateDialogFilter | UpdateDialogFilterOrder | UpdateDialogFilters | UpdatePhoneCallSignalingData | UpdateChannelMessageForwards | UpdateReadChannelDiscussionInbox | UpdateReadChannelDiscussionOutbox | UpdatePeerBlocked | UpdateChannelUserTyping | UpdatePinnedMessages | UpdatePinnedChannelMessages | UpdateChat | UpdateGroupCallParticipants | UpdateGroupCall | UpdatePeerHistoryTTL | UpdateChatParticipant | UpdateChannelParticipant | UpdateBotStopped | UpdateGroupCallConnection | UpdateBotCommands | UpdatePendingJoinRequests | UpdateBotChatInviteRequester | UpdateMessageReactions | UpdateAttachMenuBots | UpdateWebViewResultSent | UpdateBotMenuButton | UpdateSavedRingtones | UpdateTranscribedAudio | UpdateReadFeaturedEmojiStickers | UpdateUserEmojiStatus | UpdateRecentEmojiStatuses | UpdateRecentReactions | UpdateMoveStickerSetToTop | UpdateMessageExtendedMedia | UpdateChannelPinnedTopic | UpdateChannelPinnedTopics | UpdateUser | UpdateAutoSaveSettings | UpdateStory | UpdateReadStories | UpdateStoryID | UpdateStoriesStealthMode | UpdateSentStoryReaction | UpdateBotChatBoost | UpdateChannelViewForumAsMessages | UpdatePeerWallpaper | UpdateBotMessageReaction | UpdateBotMessageReactions | UpdateSavedDialogPinned | UpdatePinnedSavedDialogs | UpdateSavedReactionTags | UpdateSmsJob | UpdateQuickReplies | UpdateNewQuickReply | UpdateDeleteQuickReply | UpdateQuickReplyMessage | UpdateDeleteQuickReplyMessages | UpdateBotBusinessConnect | UpdateBotNewBusinessMessage | UpdateBotEditBusinessMessage | UpdateBotDeleteBusinessMessage | UpdateNewStoryReaction; + export type TypeUpdate = UpdateNewMessage | UpdateMessageID | UpdateDeleteMessages | UpdateUserTyping | UpdateChatUserTyping | UpdateChatParticipants | UpdateUserStatus | UpdateUserName | UpdateNewAuthorization | UpdateNewEncryptedMessage | UpdateEncryptedChatTyping | UpdateEncryption | UpdateEncryptedMessagesRead | UpdateChatParticipantAdd | UpdateChatParticipantDelete | UpdateDcOptions | UpdateNotifySettings | UpdateServiceNotification | UpdatePrivacy | UpdateUserPhone | UpdateReadHistoryInbox | UpdateReadHistoryOutbox | UpdateWebPage | UpdateReadMessagesContents | UpdateChannelTooLong | UpdateChannel | UpdateNewChannelMessage | UpdateReadChannelInbox | UpdateDeleteChannelMessages | UpdateChannelMessageViews | UpdateChatParticipantAdmin | UpdateNewStickerSet | UpdateStickerSetsOrder | UpdateStickerSets | UpdateSavedGifs | UpdateBotInlineQuery | UpdateBotInlineSend | UpdateEditChannelMessage | UpdateBotCallbackQuery | UpdateEditMessage | UpdateInlineBotCallbackQuery | UpdateReadChannelOutbox | UpdateDraftMessage | UpdateReadFeaturedStickers | UpdateRecentStickers | UpdateConfig | UpdatePtsChanged | UpdateChannelWebPage | UpdateDialogPinned | UpdatePinnedDialogs | UpdateBotWebhookJSON | UpdateBotWebhookJSONQuery | UpdateBotShippingQuery | UpdateBotPrecheckoutQuery | UpdatePhoneCall | UpdateLangPackTooLong | UpdateLangPack | UpdateFavedStickers | UpdateChannelReadMessagesContents | UpdateContactsReset | UpdateChannelAvailableMessages | UpdateDialogUnreadMark | UpdateMessagePoll | UpdateChatDefaultBannedRights | UpdateFolderPeers | UpdatePeerSettings | UpdatePeerLocated | UpdateNewScheduledMessage | UpdateDeleteScheduledMessages | UpdateTheme | UpdateGeoLiveViewed | UpdateLoginToken | UpdateMessagePollVote | UpdateDialogFilter | UpdateDialogFilterOrder | UpdateDialogFilters | UpdatePhoneCallSignalingData | UpdateChannelMessageForwards | UpdateReadChannelDiscussionInbox | UpdateReadChannelDiscussionOutbox | UpdatePeerBlocked | UpdateChannelUserTyping | UpdatePinnedMessages | UpdatePinnedChannelMessages | UpdateChat | UpdateGroupCallParticipants | UpdateGroupCall | UpdatePeerHistoryTTL | UpdateChatParticipant | UpdateChannelParticipant | UpdateBotStopped | UpdateGroupCallConnection | UpdateBotCommands | UpdatePendingJoinRequests | UpdateBotChatInviteRequester | UpdateMessageReactions | UpdateAttachMenuBots | UpdateWebViewResultSent | UpdateBotMenuButton | UpdateSavedRingtones | UpdateTranscribedAudio | UpdateReadFeaturedEmojiStickers | UpdateUserEmojiStatus | UpdateRecentEmojiStatuses | UpdateRecentReactions | UpdateMoveStickerSetToTop | UpdateMessageExtendedMedia | UpdateChannelPinnedTopic | UpdateChannelPinnedTopics | UpdateUser | UpdateAutoSaveSettings | UpdateStory | UpdateReadStories | UpdateStoryID | UpdateStoriesStealthMode | UpdateSentStoryReaction | UpdateBotChatBoost | UpdateChannelViewForumAsMessages | UpdatePeerWallpaper | UpdateBotMessageReaction | UpdateBotMessageReactions | UpdateSavedDialogPinned | UpdatePinnedSavedDialogs | UpdateSavedReactionTags | UpdateSmsJob | UpdateQuickReplies | UpdateNewQuickReply | UpdateDeleteQuickReply | UpdateQuickReplyMessage | UpdateDeleteQuickReplyMessages | UpdateBotBusinessConnect | UpdateBotNewBusinessMessage | UpdateBotEditBusinessMessage | UpdateBotDeleteBusinessMessage | UpdateNewStoryReaction | UpdateBroadcastRevenueTransactions | UpdateStarsBalance; export type TypeUpdates = UpdatesTooLong | UpdateShortMessage | UpdateShortChatMessage | UpdateShort | UpdatesCombined | Updates | UpdateShortSentMessage; export type TypeDcOption = DcOption; export type TypeConfig = Config; @@ -279,8 +279,8 @@ namespace Api { export type TypeBotMenuButton = BotMenuButtonDefault | BotMenuButtonCommands | BotMenuButton; export type TypeNotificationSound = NotificationSoundDefault | NotificationSoundNone | NotificationSoundLocal | NotificationSoundRingtone; export type TypeAttachMenuPeerType = AttachMenuPeerTypeSameBotPM | AttachMenuPeerTypeBotPM | AttachMenuPeerTypePM | AttachMenuPeerTypeChat | AttachMenuPeerTypeBroadcast; - export type TypeInputInvoice = InputInvoiceMessage | InputInvoiceSlug | InputInvoicePremiumGiftCode; - export type TypeInputStorePaymentPurpose = InputStorePaymentPremiumSubscription | InputStorePaymentGiftPremium | InputStorePaymentPremiumGiftCode | InputStorePaymentPremiumGiveaway; + export type TypeInputInvoice = InputInvoiceMessage | InputInvoiceSlug | InputInvoicePremiumGiftCode | InputInvoiceStars; + export type TypeInputStorePaymentPurpose = InputStorePaymentPremiumSubscription | InputStorePaymentGiftPremium | InputStorePaymentPremiumGiftCode | InputStorePaymentPremiumGiveaway | InputStorePaymentStars; export type TypePremiumGiftOption = PremiumGiftOption; export type TypePaymentFormMethod = PaymentFormMethod; export type TypeEmojiStatus = EmojiStatusEmpty | EmojiStatus | EmojiStatusUntil; @@ -363,6 +363,11 @@ namespace Api { export type TypeReactionNotificationsFrom = ReactionNotificationsFromContacts | ReactionNotificationsFromAll; export type TypeReactionsNotifySettings = ReactionsNotifySettings; export type TypeBroadcastRevenueBalances = BroadcastRevenueBalances; + export type TypeAvailableEffect = AvailableEffect; + export type TypeFactCheck = FactCheck; + export type TypeStarsTransactionPeer = StarsTransactionPeerUnsupported | StarsTransactionPeerAppStore | StarsTransactionPeerPlayMarket | StarsTransactionPeerPremiumBot | StarsTransactionPeerFragment | StarsTransactionPeer; + export type TypeStarsTopupOption = StarsTopupOption; + export type TypeStarsTransaction = StarsTransaction; export type TypeResPQ = ResPQ; export type TypeP_Q_inner_data = PQInnerData | PQInnerDataDc | PQInnerDataTemp | PQInnerDataTempDc; export type TypeServer_DH_Params = ServerDHParamsFail | ServerDHParamsOk; @@ -472,6 +477,7 @@ namespace Api { export type TypeDialogFilters = messages.DialogFilters; export type TypeMyStickers = messages.MyStickers; export type TypeInvitedUsers = messages.InvitedUsers; + export type TypeAvailableEffects = messages.AvailableEffectsNotModified | messages.AvailableEffects; } export namespace updates { @@ -550,15 +556,16 @@ namespace Api { } export namespace payments { - export type TypePaymentForm = payments.PaymentForm; + export type TypePaymentForm = payments.PaymentForm | payments.PaymentFormStars; export type TypeValidatedRequestedInfo = payments.ValidatedRequestedInfo; export type TypePaymentResult = payments.PaymentResult | payments.PaymentVerificationNeeded; - export type TypePaymentReceipt = payments.PaymentReceipt; + export type TypePaymentReceipt = payments.PaymentReceipt | payments.PaymentReceiptStars; export type TypeSavedInfo = payments.SavedInfo; export type TypeBankCardData = payments.BankCardData; export type TypeExportedInvoice = payments.ExportedInvoice; export type TypeCheckedGiftCode = payments.CheckedGiftCode; export type TypeGiveawayInfo = payments.GiveawayInfo | payments.GiveawayInfoResults; + export type TypeStarsStatus = payments.StarsStatus; } export namespace phone { @@ -839,7 +846,7 @@ namespace Api { photo?: Api.TypeInputWebDocument; invoice: Api.TypeInvoice; payload: bytes; - provider: string; + provider?: string; providerData: Api.TypeDataJSON; startParam?: string; extendedMedia?: Api.TypeInputMedia; @@ -850,7 +857,7 @@ namespace Api { photo?: Api.TypeInputWebDocument; invoice: Api.TypeInvoice; payload: bytes; - provider: string; + provider?: string; providerData: Api.TypeDataJSON; startParam?: string; extendedMedia?: Api.TypeInputMedia; @@ -1622,6 +1629,8 @@ namespace Api { restrictionReason?: Api.TypeRestrictionReason[]; ttlPeriod?: int; quickReplyShortcutId?: int; + effect?: long; + factcheck?: Api.TypeFactCheck; }> { // flags: undefined; out?: true; @@ -1661,6 +1670,8 @@ namespace Api { restrictionReason?: Api.TypeRestrictionReason[]; ttlPeriod?: int; quickReplyShortcutId?: int; + effect?: long; + factcheck?: Api.TypeFactCheck; }; export class MessageService extends VirtualClass<{ // flags: undefined; @@ -3774,6 +3785,18 @@ namespace Api { peer: Api.TypePeer; reaction: Api.TypeReaction; }; + export class UpdateBroadcastRevenueTransactions extends VirtualClass<{ + peer: Api.TypePeer; + balances: Api.TypeBroadcastRevenueBalances; + }> { + peer: Api.TypePeer; + balances: Api.TypeBroadcastRevenueBalances; + }; + export class UpdateStarsBalance extends VirtualClass<{ + balance: long; + }> { + balance: long; + }; export class UpdatesTooLong extends VirtualClass {}; export class UpdateShortMessage extends VirtualClass<{ // flags: undefined; @@ -5071,9 +5094,13 @@ namespace Api { documentId: long; }; export class MessageEntityBlockquote extends VirtualClass<{ + // flags: undefined; + collapsed?: true; offset: int; length: int; }> { + // flags: undefined; + collapsed?: true; offset: int; length: int; }; @@ -8581,6 +8608,11 @@ namespace Api { purpose: Api.TypeInputStorePaymentPurpose; option: Api.TypePremiumGiftCodeOption; }; + export class InputInvoiceStars extends VirtualClass<{ + option: Api.TypeStarsTopupOption; + }> { + option: Api.TypeStarsTopupOption; + }; export class InputStorePaymentPremiumSubscription extends VirtualClass<{ // flags: undefined; restore?: true; @@ -8637,6 +8669,17 @@ namespace Api { currency: string; amount: long; }; + export class InputStorePaymentStars extends VirtualClass<{ + // flags: undefined; + stars: long; + currency: string; + amount: long; + }> { + // flags: undefined; + stars: long; + currency: string; + amount: long; + }; export class PremiumGiftOption extends VirtualClass<{ // flags: undefined; months: int; @@ -9871,6 +9914,82 @@ namespace Api { availableBalance: long; overallRevenue: long; }; + export class AvailableEffect extends VirtualClass<{ + // flags: undefined; + premiumRequired?: true; + id: long; + emoticon: string; + staticIconId?: long; + effectStickerId: long; + effectAnimationId?: long; + }> { + // flags: undefined; + premiumRequired?: true; + id: long; + emoticon: string; + staticIconId?: long; + effectStickerId: long; + effectAnimationId?: long; + }; + export class FactCheck extends VirtualClass<{ + // flags: undefined; + needCheck?: true; + country?: string; + text?: Api.TypeTextWithEntities; + hash: long; + }> { + // flags: undefined; + needCheck?: true; + country?: string; + text?: Api.TypeTextWithEntities; + hash: long; + }; + export class StarsTransactionPeerUnsupported extends VirtualClass {}; + export class StarsTransactionPeerAppStore extends VirtualClass {}; + export class StarsTransactionPeerPlayMarket extends VirtualClass {}; + export class StarsTransactionPeerPremiumBot extends VirtualClass {}; + export class StarsTransactionPeerFragment extends VirtualClass {}; + export class StarsTransactionPeer extends VirtualClass<{ + peer: Api.TypePeer; + }> { + peer: Api.TypePeer; + }; + export class StarsTopupOption extends VirtualClass<{ + // flags: undefined; + extended?: true; + stars: long; + storeProduct?: string; + currency: string; + amount: long; + }> { + // flags: undefined; + extended?: true; + stars: long; + storeProduct?: string; + currency: string; + amount: long; + }; + export class StarsTransaction extends VirtualClass<{ + // flags: undefined; + refund?: true; + id: string; + stars: long; + date: int; + peer: Api.TypeStarsTransactionPeer; + title?: string; + description?: string; + photo?: Api.TypeWebDocument; + }> { + // flags: undefined; + refund?: true; + id: string; + stars: long; + date: int; + peer: Api.TypeStarsTransactionPeer; + title?: string; + description?: string; + photo?: Api.TypeWebDocument; + }; export class ResPQ extends VirtualClass<{ nonce: int128; serverNonce: int128; @@ -10356,12 +10475,14 @@ namespace Api { export class SentCodeTypeFirebaseSms extends VirtualClass<{ // flags: undefined; nonce?: bytes; + playIntegrityNonce?: bytes; receipt?: string; pushTimeout?: int; length: int; }> { // flags: undefined; nonce?: bytes; + playIntegrityNonce?: bytes; receipt?: string; pushTimeout?: int; length: int; @@ -11164,6 +11285,16 @@ namespace Api { updates: Api.TypeUpdates; missingInvitees: Api.TypeMissingInvitee[]; }; + export class AvailableEffectsNotModified extends VirtualClass {}; + export class AvailableEffects extends VirtualClass<{ + hash: int; + effects: Api.TypeAvailableEffect[]; + documents: Api.TypeDocument[]; + }> { + hash: int; + effects: Api.TypeAvailableEffect[]; + documents: Api.TypeDocument[]; + }; } export namespace updates { @@ -11921,6 +12052,25 @@ namespace Api { savedCredentials?: Api.TypePaymentSavedCredentials[]; users: Api.TypeUser[]; }; + export class PaymentFormStars extends VirtualClass<{ + // flags: undefined; + formId: long; + botId: long; + title: string; + description: string; + photo?: Api.TypeWebDocument; + invoice: Api.TypeInvoice; + users: Api.TypeUser[]; + }> { + // flags: undefined; + formId: long; + botId: long; + title: string; + description: string; + photo?: Api.TypeWebDocument; + invoice: Api.TypeInvoice; + users: Api.TypeUser[]; + }; export class ValidatedRequestedInfo extends VirtualClass<{ // flags: undefined; id?: string; @@ -11973,6 +12123,31 @@ namespace Api { credentialsTitle: string; users: Api.TypeUser[]; }; + export class PaymentReceiptStars extends VirtualClass<{ + // flags: undefined; + date: int; + botId: long; + title: string; + description: string; + photo?: Api.TypeWebDocument; + invoice: Api.TypeInvoice; + currency: string; + totalAmount: long; + transactionId: string; + users: Api.TypeUser[]; + }> { + // flags: undefined; + date: int; + botId: long; + title: string; + description: string; + photo?: Api.TypeWebDocument; + invoice: Api.TypeInvoice; + currency: string; + totalAmount: long; + transactionId: string; + users: Api.TypeUser[]; + }; export class SavedInfo extends VirtualClass<{ // flags: undefined; hasSavedCredentials?: true; @@ -12053,6 +12228,21 @@ namespace Api { winnersCount: int; activatedCount: int; }; + export class StarsStatus extends VirtualClass<{ + // flags: undefined; + balance: long; + history: Api.TypeStarsTransaction[]; + nextOffset?: string; + chats: Api.TypeChat[]; + users: Api.TypeUser[]; + }> { + // flags: undefined; + balance: long; + history: Api.TypeStarsTransaction[]; + nextOffset?: string; + chats: Api.TypeChat[]; + users: Api.TypeUser[]; + }; } export namespace phone { @@ -12615,6 +12805,24 @@ namespace Api { connectionId: string; query: X; }; + export class InvokeWithGooglePlayIntegrity extends Request, X> { + nonce: string; + token: string; + query: X; + }; + export class InvokeWithApnsSecret extends Request, X> { + nonce: string; + secret: string; + query: X; + }; export class ReqPq extends Request, Api.TypeResPQ> { @@ -12776,11 +12984,15 @@ namespace Api { newSettings?: account.TypePasswordInputSettings; }; export class ResendCode extends Request, auth.TypeSentCode> { + // flags: undefined; phoneNumber: string; phoneCodeHash: string; + reason?: string; }; export class CancelCode extends Request, Bool> { // flags: undefined; phoneNumber: string; phoneCodeHash: string; safetyNetToken?: string; + playIntegrityToken?: string; iosPushSecret?: string; }; export class ResetLoginEmail extends Request, Api.TypeUpdates> { // flags: undefined; noWebpage?: true; @@ -13927,6 +14142,7 @@ namespace Api { scheduleDate?: int; sendAs?: Api.TypeInputPeer; quickReplyShortcut?: Api.TypeInputQuickReplyShortcut; + effect?: long; }; export class SendMedia extends Request, Api.TypeUpdates> { // flags: undefined; silent?: true; @@ -13964,6 +14181,7 @@ namespace Api { scheduleDate?: int; sendAs?: Api.TypeInputPeer; quickReplyShortcut?: Api.TypeInputQuickReplyShortcut; + effect?: long; }; export class ForwardMessages extends Request, Api.TypeUpdates> { // flags: undefined; silent?: true; @@ -14771,6 +14990,7 @@ namespace Api { scheduleDate?: int; sendAs?: Api.TypeInputPeer; quickReplyShortcut?: Api.TypeInputQuickReplyShortcut; + effect?: long; }; export class UploadEncryptedFile extends Request, messages.TypeEmojiGroups> { hash: int; }; + export class GetAvailableEffects extends Request, messages.TypeAvailableEffects> { + hash: int; + }; + export class EditFactCheck extends Request, Api.TypeUpdates> { + peer: Api.TypeInputPeer; + msgId: int; + text: Api.TypeTextWithEntities; + }; + export class DeleteFactCheck extends Request, Api.TypeUpdates> { + peer: Api.TypeInputPeer; + msgId: int; + }; + export class GetFactCheck extends Request, Api.TypeFactCheck[]> { + peer: Api.TypeInputPeer; + msgId: int[]; + }; } export namespace updates { @@ -16613,6 +16861,19 @@ namespace Api { channel: Api.TypeInputChannel; restricted: Bool; }; + export class SearchPosts extends Request, messages.TypeMessages> { + hashtag: string; + offsetRate: int; + offsetPeer: Api.TypeInputPeer; + offsetId: int; + limit: int; + }; } export namespace bots { @@ -16853,6 +17114,41 @@ namespace Api { giveawayId: long; purpose: Api.TypeInputStorePaymentPurpose; }; + export class GetStarsTopupOptions extends Request {}; + export class GetStarsStatus extends Request, payments.TypeStarsStatus> { + peer: Api.TypeInputPeer; + }; + export class GetStarsTransactions extends Request, payments.TypeStarsStatus> { + // flags: undefined; + inbound?: true; + outbound?: true; + peer: Api.TypeInputPeer; + offset: string; + }; + export class SendStarsForm extends Request, payments.TypePaymentResult> { + // flags: undefined; + formId: long; + invoice: Api.TypeInputInvoice; + }; + export class RefundStarsCharge extends Request, Api.TypeUpdates> { + userId: Api.TypeInputUser; + chargeId: string; + }; } export namespace stickers { @@ -17774,19 +18070,19 @@ namespace Api { }; } - export type AnyRequest = InvokeAfterMsg | InvokeAfterMsgs | InitConnection | InvokeWithLayer | InvokeWithoutUpdates | InvokeWithMessagesRange | InvokeWithTakeout | InvokeWithBusinessConnection | ReqPq | ReqPqMulti | ReqPqMultiNew | ReqDHParams | SetClientDHParams | DestroyAuthKey | RpcDropAnswer | GetFutureSalts | Ping | PingDelayDisconnect | DestroySession + export type AnyRequest = InvokeAfterMsg | InvokeAfterMsgs | InitConnection | InvokeWithLayer | InvokeWithoutUpdates | InvokeWithMessagesRange | InvokeWithTakeout | InvokeWithBusinessConnection | InvokeWithGooglePlayIntegrity | InvokeWithApnsSecret | ReqPq | ReqPqMulti | ReqPqMultiNew | ReqDHParams | SetClientDHParams | DestroyAuthKey | RpcDropAnswer | GetFutureSalts | Ping | PingDelayDisconnect | DestroySession | auth.SendCode | auth.SignUp | auth.SignIn | auth.LogOut | auth.ResetAuthorizations | auth.ExportAuthorization | auth.ImportAuthorization | auth.BindTempAuthKey | auth.ImportBotAuthorization | auth.CheckPassword | auth.RequestPasswordRecovery | auth.RecoverPassword | auth.ResendCode | auth.CancelCode | auth.DropTempAuthKeys | auth.ExportLoginToken | auth.ImportLoginToken | auth.AcceptLoginToken | auth.CheckRecoveryPassword | auth.ImportWebTokenAuthorization | auth.RequestFirebaseSms | auth.ResetLoginEmail | auth.ReportMissingCode | account.RegisterDevice | account.UnregisterDevice | account.UpdateNotifySettings | account.GetNotifySettings | account.ResetNotifySettings | account.UpdateProfile | account.UpdateStatus | account.GetWallPapers | account.ReportPeer | account.CheckUsername | account.UpdateUsername | account.GetPrivacy | account.SetPrivacy | account.DeleteAccount | account.GetAccountTTL | account.SetAccountTTL | account.SendChangePhoneCode | account.ChangePhone | account.UpdateDeviceLocked | account.GetAuthorizations | account.ResetAuthorization | account.GetPassword | account.GetPasswordSettings | account.UpdatePasswordSettings | account.SendConfirmPhoneCode | account.ConfirmPhone | account.GetTmpPassword | account.GetWebAuthorizations | account.ResetWebAuthorization | account.ResetWebAuthorizations | account.GetAllSecureValues | account.GetSecureValue | account.SaveSecureValue | account.DeleteSecureValue | account.GetAuthorizationForm | account.AcceptAuthorization | account.SendVerifyPhoneCode | account.VerifyPhone | account.SendVerifyEmailCode | account.VerifyEmail | account.InitTakeoutSession | account.FinishTakeoutSession | account.ConfirmPasswordEmail | account.ResendPasswordEmail | account.CancelPasswordEmail | account.GetContactSignUpNotification | account.SetContactSignUpNotification | account.GetNotifyExceptions | account.GetWallPaper | account.UploadWallPaper | account.SaveWallPaper | account.InstallWallPaper | account.ResetWallPapers | account.GetAutoDownloadSettings | account.SaveAutoDownloadSettings | account.UploadTheme | account.CreateTheme | account.UpdateTheme | account.SaveTheme | account.InstallTheme | account.GetTheme | account.GetThemes | account.SetContentSettings | account.GetContentSettings | account.GetMultiWallPapers | account.GetGlobalPrivacySettings | account.SetGlobalPrivacySettings | account.ReportProfilePhoto | account.ResetPassword | account.DeclinePasswordReset | account.GetChatThemes | account.SetAuthorizationTTL | account.ChangeAuthorizationSettings | account.GetSavedRingtones | account.SaveRingtone | account.UploadRingtone | account.UpdateEmojiStatus | account.GetDefaultEmojiStatuses | account.GetRecentEmojiStatuses | account.ClearRecentEmojiStatuses | account.ReorderUsernames | account.ToggleUsername | account.GetDefaultProfilePhotoEmojis | account.GetDefaultGroupPhotoEmojis | account.GetAutoSaveSettings | account.SaveAutoSaveSettings | account.DeleteAutoSaveExceptions | account.InvalidateSignInCodes | account.UpdateColor | account.GetDefaultBackgroundEmojis | account.GetChannelDefaultEmojiStatuses | account.GetChannelRestrictedStatusEmojis | account.UpdateBusinessWorkHours | account.UpdateBusinessLocation | account.UpdateBusinessGreetingMessage | account.UpdateBusinessAwayMessage | account.UpdateConnectedBot | account.GetConnectedBots | account.GetBotBusinessConnection | account.UpdateBusinessIntro | account.ToggleConnectedBotPaused | account.DisablePeerConnectedBot | account.UpdateBirthday | account.CreateBusinessChatLink | account.EditBusinessChatLink | account.DeleteBusinessChatLink | account.GetBusinessChatLinks | account.ResolveBusinessChatLink | account.UpdatePersonalChannel | account.ToggleSponsoredMessages | account.GetReactionsNotifySettings | account.SetReactionsNotifySettings | users.GetUsers | users.GetFullUser | users.SetSecureValueErrors | users.GetIsPremiumRequiredToContact | contacts.GetContactIDs | contacts.GetStatuses | contacts.GetContacts | contacts.ImportContacts | contacts.DeleteContacts | contacts.DeleteByPhones | contacts.Block | contacts.Unblock | contacts.GetBlocked | contacts.Search | contacts.ResolveUsername | contacts.GetTopPeers | contacts.ResetTopPeerRating | contacts.ResetSaved | contacts.GetSaved | contacts.ToggleTopPeers | contacts.AddContact | contacts.AcceptContact | contacts.GetLocated | contacts.BlockFromReplies | contacts.ResolvePhone | contacts.ExportContactToken | contacts.ImportContactToken | contacts.EditCloseFriends | contacts.SetBlocked | contacts.GetBirthdays - | messages.GetMessages | messages.GetDialogs | messages.GetHistory | messages.Search | messages.ReadHistory | messages.DeleteHistory | messages.DeleteMessages | messages.ReceivedMessages | messages.SetTyping | messages.SendMessage | messages.SendMedia | messages.ForwardMessages | messages.ReportSpam | messages.GetPeerSettings | messages.Report | messages.GetChats | messages.GetFullChat | messages.EditChatTitle | messages.EditChatPhoto | messages.AddChatUser | messages.DeleteChatUser | messages.CreateChat | messages.GetDhConfig | messages.RequestEncryption | messages.AcceptEncryption | messages.DiscardEncryption | messages.SetEncryptedTyping | messages.ReadEncryptedHistory | messages.SendEncrypted | messages.SendEncryptedFile | messages.SendEncryptedService | messages.ReceivedQueue | messages.ReportEncryptedSpam | messages.ReadMessageContents | messages.GetStickers | messages.GetAllStickers | messages.GetWebPagePreview | messages.ExportChatInvite | messages.CheckChatInvite | messages.ImportChatInvite | messages.GetStickerSet | messages.InstallStickerSet | messages.UninstallStickerSet | messages.StartBot | messages.GetMessagesViews | messages.EditChatAdmin | messages.MigrateChat | messages.SearchGlobal | messages.ReorderStickerSets | messages.GetDocumentByHash | messages.GetSavedGifs | messages.SaveGif | messages.GetInlineBotResults | messages.SetInlineBotResults | messages.SendInlineBotResult | messages.GetMessageEditData | messages.EditMessage | messages.EditInlineBotMessage | messages.GetBotCallbackAnswer | messages.SetBotCallbackAnswer | messages.GetPeerDialogs | messages.SaveDraft | messages.GetAllDrafts | messages.GetFeaturedStickers | messages.ReadFeaturedStickers | messages.GetRecentStickers | messages.SaveRecentSticker | messages.ClearRecentStickers | messages.GetArchivedStickers | messages.GetMaskStickers | messages.GetAttachedStickers | messages.SetGameScore | messages.SetInlineGameScore | messages.GetGameHighScores | messages.GetInlineGameHighScores | messages.GetCommonChats | messages.GetWebPage | messages.ToggleDialogPin | messages.ReorderPinnedDialogs | messages.GetPinnedDialogs | messages.SetBotShippingResults | messages.SetBotPrecheckoutResults | messages.UploadMedia | messages.SendScreenshotNotification | messages.GetFavedStickers | messages.FaveSticker | messages.GetUnreadMentions | messages.ReadMentions | messages.GetRecentLocations | messages.SendMultiMedia | messages.UploadEncryptedFile | messages.SearchStickerSets | messages.GetSplitRanges | messages.MarkDialogUnread | messages.GetDialogUnreadMarks | messages.ClearAllDrafts | messages.UpdatePinnedMessage | messages.SendVote | messages.GetPollResults | messages.GetOnlines | messages.EditChatAbout | messages.EditChatDefaultBannedRights | messages.GetEmojiKeywords | messages.GetEmojiKeywordsDifference | messages.GetEmojiKeywordsLanguages | messages.GetEmojiURL | messages.GetSearchCounters | messages.RequestUrlAuth | messages.AcceptUrlAuth | messages.HidePeerSettingsBar | messages.GetScheduledHistory | messages.GetScheduledMessages | messages.SendScheduledMessages | messages.DeleteScheduledMessages | messages.GetPollVotes | messages.ToggleStickerSets | messages.GetDialogFilters | messages.GetSuggestedDialogFilters | messages.UpdateDialogFilter | messages.UpdateDialogFiltersOrder | messages.GetOldFeaturedStickers | messages.GetReplies | messages.GetDiscussionMessage | messages.ReadDiscussion | messages.UnpinAllMessages | messages.DeleteChat | messages.DeletePhoneCallHistory | messages.CheckHistoryImport | messages.InitHistoryImport | messages.UploadImportedMedia | messages.StartHistoryImport | messages.GetExportedChatInvites | messages.GetExportedChatInvite | messages.EditExportedChatInvite | messages.DeleteRevokedExportedChatInvites | messages.DeleteExportedChatInvite | messages.GetAdminsWithInvites | messages.GetChatInviteImporters | messages.SetHistoryTTL | messages.CheckHistoryImportPeer | messages.SetChatTheme | messages.GetMessageReadParticipants | messages.GetSearchResultsCalendar | messages.GetSearchResultsPositions | messages.HideChatJoinRequest | messages.HideAllChatJoinRequests | messages.ToggleNoForwards | messages.SaveDefaultSendAs | messages.SendReaction | messages.GetMessagesReactions | messages.GetMessageReactionsList | messages.SetChatAvailableReactions | messages.GetAvailableReactions | messages.SetDefaultReaction | messages.TranslateText | messages.GetUnreadReactions | messages.ReadReactions | messages.SearchSentMedia | messages.GetAttachMenuBots | messages.GetAttachMenuBot | messages.ToggleBotInAttachMenu | messages.RequestWebView | messages.ProlongWebView | messages.RequestSimpleWebView | messages.SendWebViewResultMessage | messages.SendWebViewData | messages.TranscribeAudio | messages.RateTranscribedAudio | messages.GetCustomEmojiDocuments | messages.GetEmojiStickers | messages.GetFeaturedEmojiStickers | messages.ReportReaction | messages.GetTopReactions | messages.GetRecentReactions | messages.ClearRecentReactions | messages.GetExtendedMedia | messages.SetDefaultHistoryTTL | messages.GetDefaultHistoryTTL | messages.SendBotRequestedPeer | messages.GetEmojiGroups | messages.GetEmojiStatusGroups | messages.GetEmojiProfilePhotoGroups | messages.SearchCustomEmoji | messages.TogglePeerTranslations | messages.GetBotApp | messages.RequestAppWebView | messages.SetChatWallPaper | messages.SearchEmojiStickerSets | messages.GetSavedDialogs | messages.GetSavedHistory | messages.DeleteSavedHistory | messages.GetPinnedSavedDialogs | messages.ToggleSavedDialogPin | messages.ReorderPinnedSavedDialogs | messages.GetSavedReactionTags | messages.UpdateSavedReactionTag | messages.GetDefaultTagReactions | messages.GetOutboxReadDate | messages.GetQuickReplies | messages.ReorderQuickReplies | messages.CheckQuickReplyShortcut | messages.EditQuickReplyShortcut | messages.DeleteQuickReplyShortcut | messages.GetQuickReplyMessages | messages.SendQuickReplyMessages | messages.DeleteQuickReplyMessages | messages.ToggleDialogFilterTags | messages.GetMyStickers | messages.GetEmojiStickerGroups + | messages.GetMessages | messages.GetDialogs | messages.GetHistory | messages.Search | messages.ReadHistory | messages.DeleteHistory | messages.DeleteMessages | messages.ReceivedMessages | messages.SetTyping | messages.SendMessage | messages.SendMedia | messages.ForwardMessages | messages.ReportSpam | messages.GetPeerSettings | messages.Report | messages.GetChats | messages.GetFullChat | messages.EditChatTitle | messages.EditChatPhoto | messages.AddChatUser | messages.DeleteChatUser | messages.CreateChat | messages.GetDhConfig | messages.RequestEncryption | messages.AcceptEncryption | messages.DiscardEncryption | messages.SetEncryptedTyping | messages.ReadEncryptedHistory | messages.SendEncrypted | messages.SendEncryptedFile | messages.SendEncryptedService | messages.ReceivedQueue | messages.ReportEncryptedSpam | messages.ReadMessageContents | messages.GetStickers | messages.GetAllStickers | messages.GetWebPagePreview | messages.ExportChatInvite | messages.CheckChatInvite | messages.ImportChatInvite | messages.GetStickerSet | messages.InstallStickerSet | messages.UninstallStickerSet | messages.StartBot | messages.GetMessagesViews | messages.EditChatAdmin | messages.MigrateChat | messages.SearchGlobal | messages.ReorderStickerSets | messages.GetDocumentByHash | messages.GetSavedGifs | messages.SaveGif | messages.GetInlineBotResults | messages.SetInlineBotResults | messages.SendInlineBotResult | messages.GetMessageEditData | messages.EditMessage | messages.EditInlineBotMessage | messages.GetBotCallbackAnswer | messages.SetBotCallbackAnswer | messages.GetPeerDialogs | messages.SaveDraft | messages.GetAllDrafts | messages.GetFeaturedStickers | messages.ReadFeaturedStickers | messages.GetRecentStickers | messages.SaveRecentSticker | messages.ClearRecentStickers | messages.GetArchivedStickers | messages.GetMaskStickers | messages.GetAttachedStickers | messages.SetGameScore | messages.SetInlineGameScore | messages.GetGameHighScores | messages.GetInlineGameHighScores | messages.GetCommonChats | messages.GetWebPage | messages.ToggleDialogPin | messages.ReorderPinnedDialogs | messages.GetPinnedDialogs | messages.SetBotShippingResults | messages.SetBotPrecheckoutResults | messages.UploadMedia | messages.SendScreenshotNotification | messages.GetFavedStickers | messages.FaveSticker | messages.GetUnreadMentions | messages.ReadMentions | messages.GetRecentLocations | messages.SendMultiMedia | messages.UploadEncryptedFile | messages.SearchStickerSets | messages.GetSplitRanges | messages.MarkDialogUnread | messages.GetDialogUnreadMarks | messages.ClearAllDrafts | messages.UpdatePinnedMessage | messages.SendVote | messages.GetPollResults | messages.GetOnlines | messages.EditChatAbout | messages.EditChatDefaultBannedRights | messages.GetEmojiKeywords | messages.GetEmojiKeywordsDifference | messages.GetEmojiKeywordsLanguages | messages.GetEmojiURL | messages.GetSearchCounters | messages.RequestUrlAuth | messages.AcceptUrlAuth | messages.HidePeerSettingsBar | messages.GetScheduledHistory | messages.GetScheduledMessages | messages.SendScheduledMessages | messages.DeleteScheduledMessages | messages.GetPollVotes | messages.ToggleStickerSets | messages.GetDialogFilters | messages.GetSuggestedDialogFilters | messages.UpdateDialogFilter | messages.UpdateDialogFiltersOrder | messages.GetOldFeaturedStickers | messages.GetReplies | messages.GetDiscussionMessage | messages.ReadDiscussion | messages.UnpinAllMessages | messages.DeleteChat | messages.DeletePhoneCallHistory | messages.CheckHistoryImport | messages.InitHistoryImport | messages.UploadImportedMedia | messages.StartHistoryImport | messages.GetExportedChatInvites | messages.GetExportedChatInvite | messages.EditExportedChatInvite | messages.DeleteRevokedExportedChatInvites | messages.DeleteExportedChatInvite | messages.GetAdminsWithInvites | messages.GetChatInviteImporters | messages.SetHistoryTTL | messages.CheckHistoryImportPeer | messages.SetChatTheme | messages.GetMessageReadParticipants | messages.GetSearchResultsCalendar | messages.GetSearchResultsPositions | messages.HideChatJoinRequest | messages.HideAllChatJoinRequests | messages.ToggleNoForwards | messages.SaveDefaultSendAs | messages.SendReaction | messages.GetMessagesReactions | messages.GetMessageReactionsList | messages.SetChatAvailableReactions | messages.GetAvailableReactions | messages.SetDefaultReaction | messages.TranslateText | messages.GetUnreadReactions | messages.ReadReactions | messages.SearchSentMedia | messages.GetAttachMenuBots | messages.GetAttachMenuBot | messages.ToggleBotInAttachMenu | messages.RequestWebView | messages.ProlongWebView | messages.RequestSimpleWebView | messages.SendWebViewResultMessage | messages.SendWebViewData | messages.TranscribeAudio | messages.RateTranscribedAudio | messages.GetCustomEmojiDocuments | messages.GetEmojiStickers | messages.GetFeaturedEmojiStickers | messages.ReportReaction | messages.GetTopReactions | messages.GetRecentReactions | messages.ClearRecentReactions | messages.GetExtendedMedia | messages.SetDefaultHistoryTTL | messages.GetDefaultHistoryTTL | messages.SendBotRequestedPeer | messages.GetEmojiGroups | messages.GetEmojiStatusGroups | messages.GetEmojiProfilePhotoGroups | messages.SearchCustomEmoji | messages.TogglePeerTranslations | messages.GetBotApp | messages.RequestAppWebView | messages.SetChatWallPaper | messages.SearchEmojiStickerSets | messages.GetSavedDialogs | messages.GetSavedHistory | messages.DeleteSavedHistory | messages.GetPinnedSavedDialogs | messages.ToggleSavedDialogPin | messages.ReorderPinnedSavedDialogs | messages.GetSavedReactionTags | messages.UpdateSavedReactionTag | messages.GetDefaultTagReactions | messages.GetOutboxReadDate | messages.GetQuickReplies | messages.ReorderQuickReplies | messages.CheckQuickReplyShortcut | messages.EditQuickReplyShortcut | messages.DeleteQuickReplyShortcut | messages.GetQuickReplyMessages | messages.SendQuickReplyMessages | messages.DeleteQuickReplyMessages | messages.ToggleDialogFilterTags | messages.GetMyStickers | messages.GetEmojiStickerGroups | messages.GetAvailableEffects | messages.EditFactCheck | messages.DeleteFactCheck | messages.GetFactCheck | updates.GetState | updates.GetDifference | updates.GetChannelDifference | photos.UpdateProfilePhoto | photos.UploadProfilePhoto | photos.DeletePhotos | photos.GetUserPhotos | photos.UploadContactProfilePhoto | upload.SaveFilePart | upload.GetFile | upload.SaveBigFilePart | upload.GetWebFile | upload.GetCdnFile | upload.ReuploadCdnFile | upload.GetCdnFileHashes | upload.GetFileHashes | help.GetConfig | help.GetNearestDc | help.GetAppUpdate | help.GetInviteText | help.GetSupport | help.SetBotUpdatesStatus | help.GetCdnConfig | help.GetRecentMeUrls | help.GetTermsOfServiceUpdate | help.AcceptTermsOfService | help.GetDeepLinkInfo | help.GetAppConfig | help.SaveAppLog | help.GetPassportConfig | help.GetSupportName | help.GetUserInfo | help.EditUserInfo | help.GetPromoData | help.HidePromoData | help.DismissSuggestion | help.GetCountriesList | help.GetPremiumPromo | help.GetPeerColors | help.GetPeerProfileColors | help.GetTimezonesList - | channels.ReadHistory | channels.DeleteMessages | channels.ReportSpam | channels.GetMessages | channels.GetParticipants | channels.GetParticipant | channels.GetChannels | channels.GetFullChannel | channels.CreateChannel | channels.EditAdmin | channels.EditTitle | channels.EditPhoto | channels.CheckUsername | channels.UpdateUsername | channels.JoinChannel | channels.LeaveChannel | channels.InviteToChannel | channels.DeleteChannel | channels.ExportMessageLink | channels.ToggleSignatures | channels.GetAdminedPublicChannels | channels.EditBanned | channels.GetAdminLog | channels.SetStickers | channels.ReadMessageContents | channels.DeleteHistory | channels.TogglePreHistoryHidden | channels.GetLeftChannels | channels.GetGroupsForDiscussion | channels.SetDiscussionGroup | channels.EditCreator | channels.EditLocation | channels.ToggleSlowMode | channels.GetInactiveChannels | channels.ConvertToGigagroup | channels.ViewSponsoredMessage | channels.GetSponsoredMessages | channels.GetSendAs | channels.DeleteParticipantHistory | channels.ToggleJoinToSend | channels.ToggleJoinRequest | channels.ReorderUsernames | channels.ToggleUsername | channels.DeactivateAllUsernames | channels.ToggleForum | channels.CreateForumTopic | channels.GetForumTopics | channels.GetForumTopicsByID | channels.EditForumTopic | channels.UpdatePinnedForumTopic | channels.DeleteTopicHistory | channels.ReorderPinnedForumTopics | channels.ToggleAntiSpam | channels.ReportAntiSpamFalsePositive | channels.ToggleParticipantsHidden | channels.ClickSponsoredMessage | channels.UpdateColor | channels.ToggleViewForumAsMessages | channels.GetChannelRecommendations | channels.UpdateEmojiStatus | channels.SetBoostsToUnblockRestrictions | channels.SetEmojiStickers | channels.ReportSponsoredMessage | channels.RestrictSponsoredMessages + | channels.ReadHistory | channels.DeleteMessages | channels.ReportSpam | channels.GetMessages | channels.GetParticipants | channels.GetParticipant | channels.GetChannels | channels.GetFullChannel | channels.CreateChannel | channels.EditAdmin | channels.EditTitle | channels.EditPhoto | channels.CheckUsername | channels.UpdateUsername | channels.JoinChannel | channels.LeaveChannel | channels.InviteToChannel | channels.DeleteChannel | channels.ExportMessageLink | channels.ToggleSignatures | channels.GetAdminedPublicChannels | channels.EditBanned | channels.GetAdminLog | channels.SetStickers | channels.ReadMessageContents | channels.DeleteHistory | channels.TogglePreHistoryHidden | channels.GetLeftChannels | channels.GetGroupsForDiscussion | channels.SetDiscussionGroup | channels.EditCreator | channels.EditLocation | channels.ToggleSlowMode | channels.GetInactiveChannels | channels.ConvertToGigagroup | channels.ViewSponsoredMessage | channels.GetSponsoredMessages | channels.GetSendAs | channels.DeleteParticipantHistory | channels.ToggleJoinToSend | channels.ToggleJoinRequest | channels.ReorderUsernames | channels.ToggleUsername | channels.DeactivateAllUsernames | channels.ToggleForum | channels.CreateForumTopic | channels.GetForumTopics | channels.GetForumTopicsByID | channels.EditForumTopic | channels.UpdatePinnedForumTopic | channels.DeleteTopicHistory | channels.ReorderPinnedForumTopics | channels.ToggleAntiSpam | channels.ReportAntiSpamFalsePositive | channels.ToggleParticipantsHidden | channels.ClickSponsoredMessage | channels.UpdateColor | channels.ToggleViewForumAsMessages | channels.GetChannelRecommendations | channels.UpdateEmojiStatus | channels.SetBoostsToUnblockRestrictions | channels.SetEmojiStickers | channels.ReportSponsoredMessage | channels.RestrictSponsoredMessages | channels.SearchPosts | bots.SendCustomRequest | bots.AnswerWebhookJSONQuery | bots.SetBotCommands | bots.ResetBotCommands | bots.GetBotCommands | bots.SetBotMenuButton | bots.GetBotMenuButton | bots.SetBotBroadcastDefaultAdminRights | bots.SetBotGroupDefaultAdminRights | bots.SetBotInfo | bots.GetBotInfo | bots.ReorderUsernames | bots.ToggleUsername | bots.CanSendMessage | bots.AllowSendMessage | bots.InvokeWebViewCustomMethod - | payments.GetPaymentForm | payments.GetPaymentReceipt | payments.ValidateRequestedInfo | payments.SendPaymentForm | payments.GetSavedInfo | payments.ClearSavedInfo | payments.GetBankCardData | payments.ExportInvoice | payments.AssignAppStoreTransaction | payments.AssignPlayMarketTransaction | payments.CanPurchasePremium | payments.GetPremiumGiftCodeOptions | payments.CheckGiftCode | payments.ApplyGiftCode | payments.GetGiveawayInfo | payments.LaunchPrepaidGiveaway + | payments.GetPaymentForm | payments.GetPaymentReceipt | payments.ValidateRequestedInfo | payments.SendPaymentForm | payments.GetSavedInfo | payments.ClearSavedInfo | payments.GetBankCardData | payments.ExportInvoice | payments.AssignAppStoreTransaction | payments.AssignPlayMarketTransaction | payments.CanPurchasePremium | payments.GetPremiumGiftCodeOptions | payments.CheckGiftCode | payments.ApplyGiftCode | payments.GetGiveawayInfo | payments.LaunchPrepaidGiveaway | payments.GetStarsTopupOptions | payments.GetStarsStatus | payments.GetStarsTransactions | payments.SendStarsForm | payments.RefundStarsCharge | stickers.CreateStickerSet | stickers.RemoveStickerFromSet | stickers.ChangeStickerPosition | stickers.AddStickerToSet | stickers.SetStickerSetThumb | stickers.CheckShortName | stickers.SuggestShortName | stickers.ChangeSticker | stickers.RenameStickerSet | stickers.DeleteStickerSet | stickers.ReplaceSticker | phone.GetCallConfig | phone.RequestCall | phone.AcceptCall | phone.ConfirmCall | phone.ReceivedCall | phone.DiscardCall | phone.SetCallRating | phone.SaveCallDebug | phone.SendSignalingData | phone.CreateGroupCall | phone.JoinGroupCall | phone.LeaveGroupCall | phone.InviteToGroupCall | phone.DiscardGroupCall | phone.ToggleGroupCallSettings | phone.GetGroupCall | phone.GetGroupParticipants | phone.CheckGroupCall | phone.ToggleGroupCallRecord | phone.EditGroupCallParticipant | phone.EditGroupCallTitle | phone.GetGroupCallJoinAs | phone.ExportGroupCallInvite | phone.ToggleGroupCallStartSubscription | phone.StartScheduledGroupCall | phone.SaveDefaultGroupCallJoinAs | phone.JoinGroupCallPresentation | phone.LeaveGroupCallPresentation | phone.GetGroupCallStreamChannels | phone.GetGroupCallStreamRtmpUrl | phone.SaveCallLog | langpack.GetLangPack | langpack.GetStrings | langpack.GetDifference | langpack.GetLanguages | langpack.GetLanguage diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 7c0191233..5a1b76bdd 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -29,7 +29,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro inputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#fb52dc99 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; +inputMediaInvoice#405fef0d flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:flags.3?string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; @@ -90,7 +90,7 @@ chatParticipants#3cbc93f8 chat_id:long participants:Vector vers chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#2357bf25 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int = Message; +message#94345242 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; @@ -360,6 +360,8 @@ updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Messag updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update; updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector qts:int = Update; updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; +updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; +updateStarsBalance#fb85198 balance:long = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference; updates.difference#f49ca0 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector state:updates.State = updates.Difference; @@ -562,7 +564,7 @@ messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; -messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; +messageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; inputChannelFromMessage#5b934f9d peer:InputPeer msg_id:int channel_id:long = InputChannel; @@ -630,7 +632,7 @@ auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeTyp auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; -auth.sentCodeTypeFirebaseSms#e57b1432 flags:# nonce:flags.0?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; +auth.sentCodeTypeFirebaseSms#13c90f17 flags:# nonce:flags.0?bytes play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; auth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType; auth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType; messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; @@ -737,10 +739,12 @@ inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w inputWebFileAudioAlbumThumbLocation#f46fe924 flags:# small:flags.2?true document:flags.0?InputDocument title:flags.1?string performer:flags.1?string = InputWebFileLocation; upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; payments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector users:Vector = payments.PaymentForm; +payments.paymentFormStars#7bf6b15c flags:# form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice users:Vector = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; payments.paymentReceipt#70c4fe03 flags:# date:int bot_id:long provider_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; +payments.paymentReceiptStars#dabbf83a flags:# date:int bot_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice currency:string total_amount:long transaction_id:string users:Vector = payments.PaymentReceipt; payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials; inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials; @@ -1094,6 +1098,7 @@ attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; inputInvoiceSlug#c326caef slug:string = InputInvoice; inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice; +inputInvoiceStars#1da33ad8 option:StarsTopupOption = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; messages.transcribedAudio#cfb9d957 flags:# pending:flags.0?true transcription_id:long text:string trial_remains_num:flags.1?int trial_remains_until_date:flags.1?int = messages.TranscribedAudio; help.premiumPromo#5334759c status_text:string status_entities:Vector video_sections:Vector videos:Vector period_options:Vector users:Vector = help.PremiumPromo; @@ -1101,6 +1106,7 @@ inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgra inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiftCode#a3805f3f flags:# users:Vector boost_peer:flags.0?InputPeer currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentStars#4f0ee8df flags:# stars:long currency:string amount:long = InputStorePaymentPurpose; premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; paymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod; emojiStatusEmpty#2de11aae = EmojiStatus; @@ -1293,6 +1299,19 @@ reactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom; reactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom; reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings; broadcastRevenueBalances#8438f1c6 current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances; +availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect; +messages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects; +messages.availableEffects#bddb616e hash:int effects:Vector documents:Vector = messages.AvailableEffects; +factCheck#b89bfccf flags:# need_check:flags.0?true country:flags.1?string text:flags.1?TextWithEntities hash:long = FactCheck; +starsTransactionPeerUnsupported#95f2bfe4 = StarsTransactionPeer; +starsTransactionPeerAppStore#b457b375 = StarsTransactionPeer; +starsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer; +starsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer; +starsTransactionPeerFragment#e92fd902 = StarsTransactionPeer; +starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; +starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; +starsTransaction#cc7079b2 flags:# refund:flags.3?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument = StarsTransaction; +payments.starsStatus#8cf4ee60 flags:# balance:long history:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X; @@ -1307,7 +1326,7 @@ auth.importAuthorization#a57a7dad id:long bytes:bytes = auth.Authorization; auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool; auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery; -auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentCode; +auth.resendCode#cae47523 flags:# phone_number:string phone_code_hash:string reason:flags.0?string = auth.SentCode; auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector = auth.LoginToken; @@ -1378,8 +1397,8 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#dff8042c flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; -messages.sendMedia#7bd66041 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; @@ -1429,7 +1448,7 @@ messages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; -messages.sendMultiMedia#c964709 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates; @@ -1504,6 +1523,7 @@ messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate; messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies; messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector hash:long = messages.Messages; messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector random_id:Vector = Updates; +messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; @@ -1583,6 +1603,10 @@ payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode; payments.applyGiftCode#f6e26854 slug:string = Updates; payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; +payments.getStarsTopupOptions#c00ec7d3 = Vector; +payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; +payments.getStarsTransactions#673ac2f9 flags:# inbound:flags.0?true outbound:flags.1?true peer:InputPeer offset:string = payments.StarsStatus; +payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payments.PaymentResult; phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 3f30966ac..3a5a5a8c8 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -174,6 +174,7 @@ "messages.getQuickReplies", "messages.getQuickReplyMessages", "messages.sendQuickReplyMessages", + "messages.getFactCheck", "updates.getState", "updates.getDifference", "updates.getChannelDifference", @@ -351,5 +352,9 @@ "payments.getGiveawayInfo", "payments.getPremiumGiftCodeOptions", "payments.launchPrepaidGiveaway", + "payments.getStarsTopupOptions", + "payments.getStarsStatus", + "payments.getStarsTransactions", + "payments.sendStarsForm", "fragment.getCollectibleInfo" ] diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index c724cf351..1d8bbf903 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -38,7 +38,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro inputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#fb52dc99 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; +inputMediaInvoice#405fef0d flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:flags.3?string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; @@ -114,7 +114,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#2357bf25 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int = Message; +message#94345242 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -413,6 +413,8 @@ updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Messag updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update; updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector qts:int = Update; updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; +updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; +updateStarsBalance#fb85198 balance:long = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -669,7 +671,7 @@ messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; -messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; +messageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; @@ -757,7 +759,7 @@ auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeTyp auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; -auth.sentCodeTypeFirebaseSms#e57b1432 flags:# nonce:flags.0?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; +auth.sentCodeTypeFirebaseSms#13c90f17 flags:# nonce:flags.0?bytes play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; auth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType; auth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType; @@ -900,6 +902,7 @@ inputWebFileAudioAlbumThumbLocation#f46fe924 flags:# small:flags.2?true document upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; payments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector users:Vector = payments.PaymentForm; +payments.paymentFormStars#7bf6b15c flags:# form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice users:Vector = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; @@ -907,6 +910,7 @@ payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; payments.paymentReceipt#70c4fe03 flags:# date:int bot_id:long provider_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; +payments.paymentReceiptStars#dabbf83a flags:# date:int bot_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice currency:string total_amount:long transaction_id:string users:Vector = payments.PaymentReceipt; payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; @@ -1446,6 +1450,7 @@ attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; inputInvoiceSlug#c326caef slug:string = InputInvoice; inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice; +inputInvoiceStars#1da33ad8 option:StarsTopupOption = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; @@ -1457,6 +1462,7 @@ inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgra inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiftCode#a3805f3f flags:# users:Vector boost_peer:flags.0?InputPeer currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentStars#4f0ee8df flags:# stars:long currency:string amount:long = InputStorePaymentPurpose; premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; @@ -1781,6 +1787,26 @@ reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNo broadcastRevenueBalances#8438f1c6 current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances; +availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect; + +messages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects; +messages.availableEffects#bddb616e hash:int effects:Vector documents:Vector = messages.AvailableEffects; + +factCheck#b89bfccf flags:# need_check:flags.0?true country:flags.1?string text:flags.1?TextWithEntities hash:long = FactCheck; + +starsTransactionPeerUnsupported#95f2bfe4 = StarsTransactionPeer; +starsTransactionPeerAppStore#b457b375 = StarsTransactionPeer; +starsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer; +starsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer; +starsTransactionPeerFragment#e92fd902 = StarsTransactionPeer; +starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; + +starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; + +starsTransaction#cc7079b2 flags:# refund:flags.3?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument = StarsTransaction; + +payments.starsStatus#8cf4ee60 flags:# balance:long history:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1791,6 +1817,8 @@ invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X; invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X; invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; invokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X; +invokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X; +invokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; auth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; @@ -1804,7 +1832,7 @@ auth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_au auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery; auth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.PasswordInputSettings = auth.Authorization; -auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentCode; +auth.resendCode#cae47523 flags:# phone_number:string phone_code_hash:string reason:flags.0?string = auth.SentCode; auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector = auth.LoginToken; @@ -1812,7 +1840,7 @@ auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken; auth.acceptLoginToken#e894ad4d token:bytes = Authorization; auth.checkRecoveryPassword#d36bf79 code:string = Bool; auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization; -auth.requestFirebaseSms#89464b50 flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string ios_push_secret:flags.1?string = Bool; +auth.requestFirebaseSms#8e39261e flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string play_integrity_token:flags.2?string ios_push_secret:flags.1?string = Bool; auth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode; auth.reportMissingCode#cb9deff6 phone_number:string phone_code_hash:string mnc:string = Bool; @@ -1970,8 +1998,8 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#dff8042c flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; -messages.sendMedia#7bd66041 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; @@ -2050,7 +2078,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#c964709 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -2170,6 +2198,10 @@ messages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector = Upda messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; messages.getMyStickers#d0b5e1fc offset_id:long limit:int = messages.MyStickers; messages.getEmojiStickerGroups#1dd840f5 hash:int = messages.EmojiGroups; +messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; +messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates; +messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates; +messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2280,6 +2312,7 @@ channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; channels.reportSponsoredMessage#af8ff6b9 channel:InputChannel random_id:bytes option:bytes = channels.SponsoredMessageReportResult; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; +channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2314,6 +2347,11 @@ payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode; payments.applyGiftCode#f6e26854 slug:string = Updates; payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; +payments.getStarsTopupOptions#c00ec7d3 = Vector; +payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; +payments.getStarsTransactions#673ac2f9 flags:# inbound:flags.0?true outbound:flags.1?true peer:InputPeer offset:string = payments.StarsStatus; +payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payments.PaymentResult; +payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 0b34cc73b..2c05bc28a 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -13,12 +13,20 @@ margin-inline-end: calc($margin - var(--scrollbar-width)); } +@mixin filter-outline($width: 0.125rem, $color) { + filter: + drop-shadow($width $width 0 $color) + drop-shadow((-$width) $width 0 $color) + drop-shadow($width (-$width) 0 $color) + drop-shadow((-$width) (-$width) 0 $color); +} + @mixin gradient-border-top($width, $cutout: 0px) { mask-image: linear-gradient(transparent $cutout, black $width); } -@mixin gradient-border-bottom($width, $cutout: 0px) { - mask-image: linear-gradient(to top, transparent $cutout, black $width); +@mixin gradient-border-bottom($height, $cutout: 0px) { + mask-image: linear-gradient(to top, transparent $cutout, black $height); } @mixin gradient-border-top-bottom($top, $bottom) { diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 2823c7674..264d76513 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -108,6 +108,7 @@ $color-message-story-mention-to: #74bcff; --color-primary-tint: rgba(var(--color-primary), 0.1); --color-green: #{$color-green}; --color-green-darker: #{color.mix($color-green, $color-black, 84%)}; + --color-success: #{$color-green}; --color-error: #{$color-error}; --color-error-shade: #{color.mix($color-error, $color-black, 92%)}; diff --git a/src/styles/icons.scss b/src/styles/icons.scss index e72ec83b3..cafa6744a 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -179,50 +179,50 @@ $icons-map: ( "play-story": "\f194", "play": "\f195", "poll": "\f196", - "premium": "\f197", - "previous": "\f198", - "privacy-policy": "\f199", - "quote-text": "\f19a", - "quote": "\f19b", - "readchats": "\f19c", - "recent": "\f19d", - "reload": "\f19e", - "remove-quote": "\f19f", - "remove": "\f1a0", - "reopen-topic": "\f1a1", - "replace": "\f1a2", - "replies": "\f1a3", - "reply-filled": "\f1a4", - "reply": "\f1a5", - "revenue-split": "\f1a6", - "revote": "\f1a7", - "save-story": "\f1a8", - "saved-messages": "\f1a9", - "schedule": "\f1aa", - "search": "\f1ab", - "select": "\f1ac", - "send-outline": "\f1ad", - "send": "\f1ae", - "settings-filled": "\f1af", - "settings": "\f1b0", - "share-filled": "\f1b1", - "share-screen-outlined": "\f1b2", - "share-screen-stop": "\f1b3", - "share-screen": "\f1b4", - "show-message": "\f1b5", - "sidebar": "\f1b6", - "skip-next": "\f1b7", - "skip-previous": "\f1b8", - "smallscreen": "\f1b9", - "smile": "\f1ba", - "sort": "\f1bb", - "speaker-muted-story": "\f1bc", - "speaker-outline": "\f1bd", - "speaker-story": "\f1be", - "speaker": "\f1bf", - "spoiler-disable": "\f1c0", - "spoiler": "\f1c1", - "sport": "\f1c2", + "previous": "\f197", + "privacy-policy": "\f198", + "quote-text": "\f199", + "quote": "\f19a", + "readchats": "\f19b", + "recent": "\f19c", + "reload": "\f19d", + "remove-quote": "\f19e", + "remove": "\f19f", + "reopen-topic": "\f1a0", + "replace": "\f1a1", + "replies": "\f1a2", + "reply-filled": "\f1a3", + "reply": "\f1a4", + "revenue-split": "\f1a5", + "revote": "\f1a6", + "save-story": "\f1a7", + "saved-messages": "\f1a8", + "schedule": "\f1a9", + "search": "\f1aa", + "select": "\f1ab", + "send-outline": "\f1ac", + "send": "\f1ad", + "settings-filled": "\f1ae", + "settings": "\f1af", + "share-filled": "\f1b0", + "share-screen-outlined": "\f1b1", + "share-screen-stop": "\f1b2", + "share-screen": "\f1b3", + "show-message": "\f1b4", + "sidebar": "\f1b5", + "skip-next": "\f1b6", + "skip-previous": "\f1b7", + "smallscreen": "\f1b8", + "smile": "\f1b9", + "sort": "\f1ba", + "speaker-muted-story": "\f1bb", + "speaker-outline": "\f1bc", + "speaker-story": "\f1bd", + "speaker": "\f1be", + "spoiler-disable": "\f1bf", + "spoiler": "\f1c0", + "sport": "\f1c1", + "star": "\f1c2", "stats": "\f1c3", "stealth-future": "\f1c4", "stealth-past": "\f1c5", @@ -718,9 +718,6 @@ $icons-map: ( .icon-poll::before { content: map.get($icons-map, "poll"); } -.icon-premium::before { - content: map.get($icons-map, "premium"); -} .icon-previous::before { content: map.get($icons-map, "previous"); } @@ -850,6 +847,9 @@ $icons-map: ( .icon-sport::before { content: map.get($icons-map, "sport"); } +.icon-star::before { + content: map.get($icons-map, "star"); +} .icon-stats::before { content: map.get($icons-map, "stats"); } diff --git a/src/styles/icons.woff b/src/styles/icons.woff index 07826423a7d709c889c02e143a35ef0b1f62c17c..918e7d860ce8d61d0520206cb9e1554da5befc99 100644 GIT binary patch delta 25747 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 WH8`f}|BX~hnCYy&z5fAhA?PhZ@c9M+ delta 25830 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 B9ijjL diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index af2268d2223464d1de50071ec65f244855709900..c93254abd507e0f2228e092e2250d3c4a75891b7 100644 GIT binary patch literal 25252 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 XX0g^$+_!%=to@tn2`mFD=!g#hb~Rv< diff --git a/src/styles/index.scss b/src/styles/index.scss index 6c7c39077..0e71b8817 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -359,6 +359,7 @@ body:not(.is-ios) { --color-default-shadow: rgb(16, 16, 16, 0.612); --color-light-shadow: rgb(0, 0, 0, 0.251); --color-green: rgb(135, 116, 225); + --color-success: rgb(0, 199, 62); --color-text-meta-colored: rgb(131, 120, 219); --color-reply-hover: rgb(39, 39, 39); --color-reply-active: rgb(46, 47, 47); diff --git a/src/styles/themes.json b/src/styles/themes.json index 081f4f5d1..bb4cdd9cf 100644 --- a/src/styles/themes.json +++ b/src/styles/themes.json @@ -33,6 +33,7 @@ "--color-light-shadow": ["#7272722B", "#00000040"], "--color-green": ["#00C73E", "#8774E1"], "--color-green-darker": ["#00a734", "#7b71c6"], + "--color-success": ["#00C73E", "#00C73E"], "--color-text-meta-colored": ["#4DCD5E", "#8378DB"], "--color-reply-hover": ["#F4F4F4", "#272727"], "--color-reply-active": ["#E8E9E9", "#2E2F2F"], diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index 27999d7c9..ba2344d54 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -149,7 +149,6 @@ export type FontIconName = | 'play-story' | 'play' | 'poll' - | 'premium' | 'previous' | 'privacy-policy' | 'quote-text' @@ -193,6 +192,7 @@ export type FontIconName = | 'spoiler-disable' | 'spoiler' | 'sport' + | 'star' | 'stats' | 'stealth-future' | 'stealth-past' diff --git a/src/types/index.ts b/src/types/index.ts index 0cfaf8c00..5e52b1941 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -478,7 +478,6 @@ export type InlineBotSettings = { export type CustomPeerType = 'premium' | 'toBeDistributed'; export interface CustomPeer { - type: CustomPeerType; isCustomPeer: true; titleKey: string; subtitleKey?: string; @@ -487,3 +486,7 @@ export interface CustomPeer { peerColorId?: number; withPremiumGradient?: boolean; } + +export interface UniqueCustomPeer extends CustomPeer { + type: CustomPeerType; +} diff --git a/src/util/element/calcTextLineHeightAndCount.ts b/src/util/element/calcTextLineHeightAndCount.ts new file mode 100644 index 000000000..625977a70 --- /dev/null +++ b/src/util/element/calcTextLineHeightAndCount.ts @@ -0,0 +1,10 @@ +export default function calcTextLineHeightAndCount(textContainer: HTMLElement) { + const lineHeight = parseInt(getComputedStyle(textContainer).lineHeight, 10); + + const totalLines = textContainer.scrollHeight / lineHeight; + + return { + totalLines, + lineHeight, + }; +} diff --git a/src/util/formatCurrency.ts b/src/util/formatCurrency.ts index 4a7bf9468..d859bafa6 100644 --- a/src/util/formatCurrency.ts +++ b/src/util/formatCurrency.ts @@ -1,5 +1,7 @@ import type { LangCode } from '../types'; +const STARS_CODE = 'XTR'; + export function formatCurrency( totalPrice: number, currency: string, @@ -8,6 +10,10 @@ export function formatCurrency( ) { const price = totalPrice / 10 ** getCurrencyExp(currency); + if (currency === STARS_CODE) { + return `⭐️${price}`; + } + if (shouldOmitFractions && price % 1 === 0) { return new Intl.NumberFormat(locale, { style: 'currency', @@ -35,7 +41,7 @@ function getCurrencyExp(currency: string) { } if ([ 'BIF', 'BYR', 'CLP', 'CVE', 'DJF', 'GNF', 'ISK', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'UYI', - 'VND', 'VUV', 'XAF', 'XOF', 'XPF', + 'VND', 'VUV', 'XAF', 'XOF', 'XPF', STARS_CODE, ].includes(currency)) { return 0; } diff --git a/src/util/objects/customPeer.ts b/src/util/objects/customPeer.ts index a0c47d569..9088a02e9 100644 --- a/src/util/objects/customPeer.ts +++ b/src/util/objects/customPeer.ts @@ -1,16 +1,16 @@ -import type { CustomPeer } from '../../types'; +import type { UniqueCustomPeer } from '../../types'; -export const CUSTOM_PEER_PREMIUM: CustomPeer = { +export const CUSTOM_PEER_PREMIUM: UniqueCustomPeer = { isCustomPeer: true, type: 'premium', titleKey: 'PrivacyPremium', subtitleKey: 'PrivacyPremiumText', - avatarIcon: 'premium', + avatarIcon: 'star', isAvatarSquare: true, withPremiumGradient: true, }; -export const CUSTOM_PEER_TO_BE_DISTRIBUTED: CustomPeer = { +export const CUSTOM_PEER_TO_BE_DISTRIBUTED: UniqueCustomPeer = { isCustomPeer: true, type: 'toBeDistributed', titleKey: 'BoostingToBeDistributed',