From 28cecbfe3c552faaec264eb5bf43170cfc2026a8 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:53:06 +0200 Subject: [PATCH] Support subscription invites (#5024) --- src/api/gramjs/apiBuilders/chats.ts | 52 +++- src/api/gramjs/apiBuilders/payments.ts | 21 ++ src/api/gramjs/gramjsBuilders/index.ts | 6 + src/api/gramjs/methods/chats.ts | 43 +--- src/api/gramjs/methods/payments.ts | 85 ++++++- src/api/types/chats.ts | 2 + src/api/types/messages.ts | 18 +- src/api/types/misc.ts | 35 ++- src/api/types/payments.ts | 13 + src/api/types/updates.ts | 9 +- src/assets/localization/fallback.strings | 10 +- src/bundles/extra.ts | 7 +- src/bundles/stars.ts | 7 + src/components/common/Avatar.tsx | 3 + .../common/FullNameTitle.module.scss | 2 +- src/components/common/FullNameTitle.tsx | 6 +- src/components/common/PeerBadge.tsx | 12 +- src/components/common/helpers/peerColor.ts | 2 +- .../common/pickers/PickerSelectedItem.tsx | 2 +- src/components/left/main/Chat.scss | 34 ++- src/components/left/main/Chat.tsx | 10 +- src/components/main/Dialogs.tsx | 84 +------ .../main/premium/StarsGiftModal.async.tsx | 2 +- .../premium/StarsGiftingPickerModal.async.tsx | 2 +- src/components/modals/ModalContainer.tsx | 8 +- .../chatInvite/ChatInviteModal.async.tsx | 18 ++ .../chatInvite/ChatInviteModal.module.scss | 31 +++ .../modals/chatInvite/ChatInviteModal.tsx | 108 ++++++++ .../modals/common/TableInfoModal.module.scss | 52 +--- .../modals/common/TableInfoModal.tsx | 44 +--- .../modals/giftcode/GiftCodeModal.module.scss | 7 + .../modals/giftcode/GiftCodeModal.tsx | 2 +- .../paidReaction/PaidReactionModal.async.tsx | 2 +- .../modals/stars/StarsBalanceModal.async.tsx | 2 +- .../stars/StarsBalanceModal.module.scss | 39 ++- .../modals/stars/StarsBalanceModal.tsx | 36 ++- .../modals/stars/StarsPaymentModal.async.tsx | 2 +- .../modals/stars/StarsPaymentModal.tsx | 81 ++++-- .../StarsSubscriptionItem.module.scss | 69 ++++++ .../subscription/StarsSubscriptionItem.tsx | 89 +++++++ .../StarsSubscriptionModal.async.tsx | 18 ++ .../StarsSubscriptionModal.module.scss | 72 ++++++ .../subscription/StarsSubscriptionModal.tsx | 230 ++++++++++++++++++ .../StarsTransactionItem.module.scss | 22 +- .../transaction/StarsTransactionItem.tsx | 33 +-- .../StarsTransactionModal.async.tsx | 2 +- .../StarsTransactionModal.module.scss | 27 +- .../transaction/StarsTransactionModal.tsx | 99 ++++---- .../right/statistics/BoostStatistics.tsx | 30 ++- src/components/ui/InfiniteScroll.tsx | 52 +++- src/config.ts | 2 +- src/global/actions/api/chats.ts | 99 ++++++-- src/global/actions/api/payments.ts | 83 +++++-- src/global/actions/apiUpdaters/chats.ts | 16 +- src/global/actions/apiUpdaters/misc.ts | 2 +- src/global/actions/apiUpdaters/payments.ts | 41 +++- src/global/actions/ui/chats.ts | 7 + src/global/actions/ui/payments.ts | 37 ++- src/global/helpers/chats.ts | 18 +- src/global/helpers/payments.ts | 9 + src/global/reducers/chats.ts | 1 + src/global/reducers/payments.ts | 24 ++ src/global/types.ts | 42 +++- src/lib/gramjs/client/TelegramClient.js | 7 +- src/lib/gramjs/tl/apiTl.js | 3 + src/lib/gramjs/tl/static/api.json | 3 + src/styles/_mixins.scss | 8 +- src/types/index.ts | 23 +- src/types/language.d.ts | 17 +- src/util/deeplink.ts | 4 +- src/util/moduleLoader.ts | 5 + src/util/objects/customPeer.ts | 16 -- 72 files changed, 1622 insertions(+), 487 deletions(-) create mode 100644 src/bundles/stars.ts create mode 100644 src/components/modals/chatInvite/ChatInviteModal.async.tsx create mode 100644 src/components/modals/chatInvite/ChatInviteModal.module.scss create mode 100644 src/components/modals/chatInvite/ChatInviteModal.tsx create mode 100644 src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss create mode 100644 src/components/modals/stars/subscription/StarsSubscriptionItem.tsx create mode 100644 src/components/modals/stars/subscription/StarsSubscriptionModal.async.tsx create mode 100644 src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss create mode 100644 src/components/modals/stars/subscription/StarsSubscriptionModal.tsx diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 9d00e4216..1b3bc278b 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -8,6 +8,7 @@ import type { ApiChatBannedRights, ApiChatFolder, ApiChatInviteImporter, + ApiChatInviteInfo, ApiChatlistExportedInvite, ApiChatlistInvite, ApiChatMember, @@ -18,13 +19,14 @@ import type { ApiRestrictionReason, ApiSendAsPeerId, ApiSponsoredMessageReportResult, + ApiStarsSubscriptionPricing, ApiTopic, } from '../../types'; import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; -import { serializeBytes } from '../helpers'; -import { buildApiUsernames, buildAvatarPhotoId } from './common'; +import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers'; +import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common'; import { omitVirtualClassFields } from './helpers'; import { buildApiEmojiStatus, @@ -67,6 +69,7 @@ function buildApiChatFieldsFromPeerEntity( ? buildApiEmojiStatus(peerEntity.emojiStatus) : undefined; const boostLevel = ('level' in peerEntity) ? peerEntity.level : undefined; const areProfilesShown = Boolean('signatureProfiles' in peerEntity && peerEntity.signatureProfiles); + const subscriptionUntil = 'subscriptionUntilDate' in peerEntity ? peerEntity.subscriptionUntilDate : undefined; return omitUndefined({ isMin, @@ -100,6 +103,7 @@ function buildApiChatFieldsFromPeerEntity( hasStories: Boolean(maxStoryId) && !storiesUnavailable, emojiStatus, boostLevel, + subscriptionUntil, }); } @@ -670,3 +674,47 @@ export function buildApiSponsoredMessageReportResult( options, }; } + +export function buildApiChatInviteInfo(invite: GramJs.ChatInvite): ApiChatInviteInfo { + const { + color, participants, participantsCount, photo, title, about, scam, fake, verified, megagroup, channel, broadcast, + requestNeeded, subscriptionFormId, subscriptionPricing, canRefulfillSubscription, + } = invite; + + let apiPhoto; + if (photo instanceof GramJs.Photo) { + addPhotoToLocalDb(photo); + apiPhoto = buildApiPhoto(photo); + } + + participants?.forEach(addUserToLocalDb); + + return { + title, + about, + isFake: fake, + isScam: scam, + isVerified: verified, + isSuperGroup: megagroup, + isPublic: invite.public, + participantsCount, + color, + isChannel: channel, + isBroadcast: broadcast, + isRequestNeeded: requestNeeded, + photo: apiPhoto, + subscriptionFormId: subscriptionFormId?.toString(), + subscriptionPricing: subscriptionPricing && buildApiStarsSubscriptionPricing(subscriptionPricing), + canRefulfillSubscription, + participantIds: participants?.map((participant) => buildApiPeerId(participant.id, 'user')).filter(Boolean), + }; +} + +export function buildApiStarsSubscriptionPricing( + pricing: GramJs.StarsSubscriptionPricing, +): ApiStarsSubscriptionPricing { + return { + period: pricing.period, + amount: pricing.amount.toJSNumber(), + }; +} diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 90fe40bfa..018f21351 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -20,6 +20,7 @@ import type { ApiReceipt, ApiStarGiveawayOption, ApiStarsGiveawayWinnerOption, + ApiStarsSubscription, ApiStarsTransaction, ApiStarsTransactionPeer, ApiStarTopupOption, @@ -27,6 +28,7 @@ import type { } from '../../types'; import { addWebDocumentToLocalDb } from '../helpers'; +import { buildApiStarsSubscriptionPricing } from './chats'; import { buildApiMessageEntity } from './common'; import { omitVirtualClassFields } from './helpers'; import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent'; @@ -504,6 +506,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction { const { date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction, + subscriptionPeriod, } = transaction; if (photo) { @@ -527,10 +530,28 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): messageId: msgId, isGift: gift, extendedMedia: boughtExtendedMedia, + subscriptionPeriod, isReaction: reaction, }; } +export function buildApiStarsSubscription(subscription: GramJs.StarsSubscription): ApiStarsSubscription { + const { + id, peer, pricing, untilDate, canRefulfill, canceled, chatInviteHash, missingBalance, + } = subscription; + + return { + id, + peerId: getApiChatIdFromMtpPeer(peer), + until: untilDate, + pricing: buildApiStarsSubscriptionPricing(pricing), + isCancelled: canceled, + canRefulfill, + hasMissingBalance: missingBalance, + chatInviteHash, + }; +} + export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): ApiStarTopupOption { const { amount, currency, stars, extended, diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 9fafdacbd..ebfa144aa 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -647,6 +647,12 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) { }); } + case 'chatInviteSubscription': { + return new GramJs.InputInvoiceChatInviteSubscription({ + hash: invoice.hash, + }); + } + case 'giveaway': default: { const purpose = buildInputStorePaymentPurpose(invoice.purpose); diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 6bd400f79..48e10734c 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -38,6 +38,7 @@ import { buildApiChatFromDialog, buildApiChatFromPreview, buildApiChatFromSavedDialog, + buildApiChatInviteInfo, buildApiChatlistExportedInvite, buildApiChatlistInvite, buildApiChatReactions, @@ -1404,53 +1405,27 @@ export async function migrateChat(chat: ApiChat) { return buildApiChatFromPreview(newChannel); } -export async function openChatByInvite(hash: string) { +export async function checkChatInvite(hash: string) { const result = await invokeRequest(new GramJs.messages.CheckChatInvite({ hash })); if (!result) { return undefined; } - let chat: ApiChat | undefined; - if (result instanceof GramJs.ChatInvite) { - const { - photo, participantsCount, title, channel, requestNeeded, about, megagroup, - } = result; - - if (photo instanceof GramJs.Photo) { - addPhotoToLocalDb(result.photo); - } - - sendApiUpdate({ - '@type': 'showInvite', - data: { - title, - about, - hash, - participantsCount, - isChannel: channel && !megagroup, - isRequestNeeded: requestNeeded, - ...(photo instanceof GramJs.Photo && { photo: buildApiPhoto(photo) }), - }, - }); - } else { - chat = buildApiChatFromPreview(result.chat); - - if (chat) { - sendApiUpdate({ - '@type': 'updateChat', - id: chat.id, - chat, - }); - } + return { + chat: undefined, + invite: buildApiChatInviteInfo(result), + users: result.participants?.map(buildApiUser).filter(Boolean), + }; } + const chat = buildApiChatFromPreview(result.chat); if (!chat) { return undefined; } - return { chatId: chat.id }; + return { chat, invite: undefined, users: undefined }; } export async function addChatMembers(chat: ApiChat, users: ApiUser[]) { diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index 906175fed..7a1aed1ee 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -20,10 +20,12 @@ import { buildApiReceipt, buildApiStarsGiftOptions, buildApiStarsGiveawayOptions, + buildApiStarsSubscription, buildApiStarsTransaction, buildApiStarTopupOption, buildShippingOptions, } from '../apiBuilders/payments'; +import { buildApiPeerId } from '../apiBuilders/peers'; import { buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo, } from '../gramjsBuilders'; @@ -134,7 +136,7 @@ export async function sendStarPaymentForm({ invoice: buildInputInvoice(inputInvoice), })); - if (!result) return false; + if (!result) return undefined; if (result instanceof GramJs.payments.PaymentVerificationNeeded) { if (DEBUG) { @@ -143,11 +145,29 @@ export async function sendStarPaymentForm({ } return undefined; - } else { - handleGramJsUpdate(result.updates); } - return Boolean(result); + handleGramJsUpdate(result.updates); + + if (inputInvoice.type === 'chatInviteSubscription') { + const updates = 'updates' in result.updates ? result.updates.updates : undefined; + + const mtpChannelId = updates?.find((update): update is GramJs.UpdateChannel => ( + update instanceof GramJs.UpdateChannel + ))?.channelId; + + if (!mtpChannelId) { + return undefined; + } + + return { + channelId: buildApiPeerId(mtpChannelId, 'channel'), + }; + } + + return { + completed: true, + }; } export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice, theme?: ApiThemeParameters) { @@ -416,8 +436,10 @@ export async function fetchStarsStatus() { } return { - nextOffset: result.nextOffset, + nextHistoryOffset: result.nextOffset, history: result.history?.map(buildApiStarsTransaction), + nextSubscriptionOffset: result.subscriptionsNextOffset, + subscriptions: result.subscriptions?.map(buildApiStarsSubscription), balance: result.balance.toJSNumber(), }; } @@ -475,6 +497,59 @@ export async function fetchStarsTransactionById({ }; } +export async function fetchStarsSubscriptions({ + offset, peer, +}: { + offset?: string; + peer?: ApiPeer; +}) { + const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(); + const result = await invokeRequest(new GramJs.payments.GetStarsSubscriptions({ + peer: inputPeer, + offset, + })); + + if (!result?.subscriptions) { + return undefined; + } + + return { + nextOffset: result.subscriptionsNextOffset, + subscriptions: result.subscriptions.map(buildApiStarsSubscription), + balance: result.balance.toJSNumber(), + }; +} + +export async function changeStarsSubscription({ + peer, subscriptionId, isCancelled, +}: { + peer?: ApiPeer; + subscriptionId: string; + isCancelled: boolean; +}) { + const result = await invokeRequest(new GramJs.payments.ChangeStarsSubscription({ + peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(), + subscriptionId, + canceled: isCancelled, + })); + + return result; +} + +export async function fulfillStarsSubscription({ + peer, subscriptionId, +}: { + peer?: ApiPeer; + subscriptionId: string; +}) { + const result = await invokeRequest(new GramJs.payments.FulfillStarsSubscription({ + peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(), + subscriptionId, + })); + + return result; +} + export async function fetchStarsTopupOptions() { const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions()); diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 82f2b129c..a3a4bdb55 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -86,6 +86,8 @@ export interface ApiChat { hasUnreadStories?: boolean; maxStoryId?: number; + subscriptionUntil?: number; + // Locally determined field detectedLanguage?: string; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index df5a23310..cf791df92 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -2,6 +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 { ApiChatInviteInfo } from './misc'; import type { ApiInputStorePaymentPurpose, ApiPremiumGiftCodeOption, @@ -263,8 +264,15 @@ export type ApiInputInvoiceStarsGiveaway = { users: number; }; +export type ApiInputInvoiceChatInviteSubscription = { + type: 'chatInviteSubscription'; + hash: string; + inviteInfo: ApiChatInviteInfo; +}; + export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway -| ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars | ApiInputInvoiceStarsGiveaway; +| ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars | ApiInputInvoiceStarsGiveaway +| ApiInputInvoiceChatInviteSubscription; /* Used for Invoice request */ export type ApiRequestInputInvoiceMessage = { @@ -294,8 +302,14 @@ export type ApiRequestInputInvoiceStarsGiveaway = { purpose: ApiInputStorePaymentPurpose; }; +export type ApiRequestInputInvoiceChatInviteSubscription = { + type: 'chatInviteSubscription'; + hash: string; +}; + export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug -| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway; +| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway +| ApiRequestInputInvoiceChatInviteSubscription; export interface ApiInvoice { mediaType: 'invoice'; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 51cc444bc..dc519f0a2 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -134,16 +134,6 @@ export type ApiFieldError = { message: string; }; -export type ApiInviteInfo = { - title: string; - about?: string; - hash: string; - isChannel?: boolean; - participantsCount?: number; - isRequestNeeded?: true; - photo?: ApiPhoto; -}; - export type ApiExportedInvite = { isRevoked?: boolean; isPermanent?: boolean; @@ -159,6 +149,31 @@ export type ApiExportedInvite = { adminId: string; }; +export type ApiChatInviteInfo = { + title: string; + about?: string; + photo?: ApiPhoto; + isScam?: boolean; + isFake?: boolean; + isChannel?: boolean; + isVerified?: boolean; + isSuperGroup?: boolean; + isPublic?: boolean; + participantsCount?: number; + participantIds?: string[]; + color: number; + isBroadcast?: boolean; + isRequestNeeded?: boolean; + subscriptionFormId?: string; + canRefulfillSubscription?: boolean; + subscriptionPricing?: ApiStarsSubscriptionPricing; +}; + +export type ApiStarsSubscriptionPricing = { + period: number; + amount: number; +}; + export type ApiChatInviteImporter = { userId: string; date: number; diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index 869d933fc..6460466c1 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -5,6 +5,7 @@ import type { ApiChat } from './chats'; import type { ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia, } from './messages'; +import type { ApiStarsSubscriptionPricing } from './misc'; import type { StatisticsOverviewPercentage } from './statistics'; import type { ApiUser } from './users'; @@ -311,6 +312,18 @@ export interface ApiStarsTransaction { description?: string; photo?: ApiWebDocument; extendedMedia?: BoughtPaidMedia[]; + subscriptionPeriod?: number; +} + +export interface ApiStarsSubscription { + id: string; + peerId: string; + until: number; + pricing: ApiStarsSubscriptionPricing; + isCancelled?: true; + canRefulfill?: true; + hasMissingBalance?: true; + chatInviteHash?: string; } export interface ApiStarTopupOption { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 8adf0f6af..85e59bf53 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -33,7 +33,7 @@ import type { BoughtPaidMedia, } from './messages'; import type { - ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData, + ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData, } from './misc'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { @@ -114,11 +114,6 @@ export type ApiUpdateChatJoin = { id: string; }; -export type ApiUpdateShowInvite = { - '@type': 'showInvite'; - data: ApiInviteInfo; -}; - export type ApiUpdateChatLeave = { '@type': 'updateChatLeave'; id: string; @@ -788,7 +783,7 @@ export type ApiUpdate = ( ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | ApiUpdateTwoFaError | ApiUpdatePasswordError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | - ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | + ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams | ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId | ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted | diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 11243d67f..56e28c813 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1281,7 +1281,9 @@ "AriaSearchOlderResult" = "Focus next result"; "AriaSearchNewerResult" = "Focus previous result"; "CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}" -"CreditsBoxOutAbout" = "Review the {link} for Stars." +"StarsTransactionTOS" = "Review the {link} for Stars." +"StarsTransactionTOSLinkText" = "Terms of Service" +"StarsTransactionTOSLink" = "https://telegram.org/tos/stars" "GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram." "SendPaidReaction" = "Send ⭐️{amount}" "StarsReactionTerms" = "By sending Stars you agree to the {link}" @@ -1290,3 +1292,9 @@ "MiniAppsMoreTabs_one" = "{botName} & {count} Other"; "MiniAppsMoreTabs_other" = "{botName} & {count} Others"; "PrizeCredits" = "Your prize is {count} Stars." +"StarsSubscribeText_one" = "Do you want to subscribe to **{chat}** for **{amount} Star** per month?" +"StarsSubscribeText_other" = "Do you want to subscribe to **{chat}** for **{amount} Stars** per month?" +"StarsSubscribeInfo" = "By subscribing you agree to the {link}" +"StarsSubscribeInfoLinkText" = "Terms of Service" +"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars" +"StarsPerMonth" = "⭐️{amount}/month" diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 8bab60b36..a2f63782a 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -18,19 +18,14 @@ export { default as AttachBotInstallModal } from '../components/modals/attachBot export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog'; export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal'; export { default as PremiumGiftModal } from '../components/main/premium/PremiumGiftModal'; -export { default as StarsGiftModal } from '../components/main/premium/StarsGiftModal'; export { default as GiveawayModal } from '../components/main/premium/GiveawayModal'; export { default as PremiumGiftingPickerModal } from '../components/main/premium/PremiumGiftingPickerModal'; -export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal'; export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal'; export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu'; export { default as BoostModal } from '../components/modals/boost/BoostModal'; export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal'; export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal'; -export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal'; -export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal'; -export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal'; -export { default as PaidReactionModal } from '../components/modals/paidReaction/PaidReactionModal'; +export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal'; export { default as AboutAdsModal } from '../components/common/AboutAdsModal'; export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal'; diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts new file mode 100644 index 000000000..af74a2adb --- /dev/null +++ b/src/bundles/stars.ts @@ -0,0 +1,7 @@ +export { default as StarsGiftModal } from '../components/main/premium/StarsGiftModal'; +export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal'; +export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal'; +export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal'; +export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal'; +export { default as StarsSubscriptionModal } from '../components/modals/stars/subscription/StarsSubscriptionModal'; +export { default as PaidReactionModal } from '../components/modals/paidReaction/PaidReactionModal'; diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index dbd5c1074..7fc41872b 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -220,6 +220,9 @@ const Avatar: FC = ({ } else if (chat) { const title = getChatTitle(lang, chat); content = title && getFirstLetters(title, isUserId(chat.id) ? 2 : 1); + } else if (isCustomPeer) { + const title = peer.title || lang(peer.titleKey!); + content = title && getFirstLetters(title, 1); } else if (text) { content = getFirstLetters(text, 2); } diff --git a/src/components/common/FullNameTitle.module.scss b/src/components/common/FullNameTitle.module.scss index e3a0240cf..dcfbf9595 100644 --- a/src/components/common/FullNameTitle.module.scss +++ b/src/components/common/FullNameTitle.module.scss @@ -9,7 +9,7 @@ } .fullName { - font-size: 1rem; + font-size: 1em; margin-bottom: 0; &.canCopy { diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index 878b1584c..c43a05abf 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -80,7 +80,7 @@ const FullNameTitle: FC = ({ const specialTitle = useMemo(() => { if (customPeer) { - return lang(customPeer.titleKey, customPeer.titleValue, 'i'); + return customPeer.title || lang(customPeer.titleKey!); } if (isSavedMessages) { @@ -125,8 +125,8 @@ const FullNameTitle: FC = ({ {!iconElement && peer && ( <> - {!noVerified && realPeer?.isVerified && } - {!noFake && realPeer?.fakeType && } + {!noVerified && peer?.isVerified && } + {!noFake && peer?.fakeType && } {withEmojiStatus && realPeer?.emojiStatus && (
-
- {badgeIcon && } - {badgeText} -
+ {badgeText && ( +
+ {badgeIcon && } + {badgeText} +
+ )}
{text &&

{text}

} diff --git a/src/components/common/helpers/peerColor.ts b/src/components/common/helpers/peerColor.ts index 831b8b6b6..4c606fe7b 100644 --- a/src/components/common/helpers/peerColor.ts +++ b/src/components/common/helpers/peerColor.ts @@ -10,7 +10,7 @@ export function getPeerColorClass(peer?: ApiPeer | CustomPeer, noUserColors?: bo } if ('isCustomPeer' in peer) { - if (!peer.peerColorId) return undefined; + if (peer.peerColorId === undefined) return undefined; return `peer-color-${peer.peerColorId}`; } return noUserColors ? `peer-color-count-${getPeerColorCount(peer)}` : `peer-color-${getPeerColorKey(peer)}`; diff --git a/src/components/common/pickers/PickerSelectedItem.tsx b/src/components/common/pickers/PickerSelectedItem.tsx index 2e318730f..93538baee 100644 --- a/src/components/common/pickers/PickerSelectedItem.tsx +++ b/src/components/common/pickers/PickerSelectedItem.tsx @@ -80,7 +80,7 @@ const PickerSelectedItem = ({ /> ); - const name = (customPeer && lang(customPeer.titleKey)) + const name = (customPeer && (customPeer.title || lang(customPeer.titleKey!))) || (!chat || (user && !isSavedMessages) ? getUserFirstOrLastName(user) : getChatTitle(lang, chat, isSavedMessages)); diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index 4491cb835..af59b87b5 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -1,3 +1,5 @@ +@use "../../../styles/mixins"; + .Chat { --background-color: var(--color-background); --thumbs-background: var(--background-color); @@ -39,8 +41,8 @@ &:hover, &.ListItem.has-menu-open { - .avatar-online { - border-color: var(--color-chat-hover); + .avatar-badge { + --_color-outline: var(--color-chat-hover); } .avatar-badge-wrapper { @@ -70,8 +72,8 @@ &.selected { --background-color: var(--color-chat-hover) !important; - .avatar-online { - border-color: var(--color-chat-hover); + .avatar-badge { + --_color-outline: var(--color-chat-hover); } .ChatCallStatus { @@ -94,8 +96,11 @@ --color-checkmark: var(--color-primary); } + .avatar-badge { + --_color-outline: var(--color-chat-active) !important; + } + .avatar-online { - border-color: var(--color-chat-active) !important; background-color: var(--color-white); } @@ -241,16 +246,25 @@ } } - .avatar-online { + .avatar-badge { + --_color-outline: var(--color-background); position: absolute; bottom: 0.0625rem; right: 0.0625rem; + flex-shrink: 0; + } + + .avatar-subscription { + @include mixins.filter-outline(1px, var(--_color-outline)); + } + + .avatar-online { + border-radius: 50%; + border: 2px solid var(--_color-outline); + background-color: #0ac630; + width: 0.875rem; height: 0.875rem; - border-radius: 50%; - border: 2px solid var(--color-background); - background-color: #0ac630; - flex-shrink: 0; opacity: 0.5; transform: scale(0); diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 321b02d5a..b41f0c25e 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -64,6 +64,7 @@ import useChatListEntry from './hooks/useChatListEntry'; import Avatar from '../../common/Avatar'; import DeleteChatModal from '../../common/DeleteChatModal'; import FullNameTitle from '../../common/FullNameTitle'; +import StarIcon from '../../common/icons/StarIcon'; import LastMessageMeta from '../../common/LastMessageMeta'; import ReportModal from '../../common/ReportModal'; import ListItem from '../../ui/ListItem'; @@ -345,12 +346,17 @@ const Chat: FC = ({ isSavedDialog={isSavedDialog} size={isPreview ? 'medium' : 'large'} withStory={!user?.isSelf} - withStoryGap={isAvatarOnlineShown} + withStoryGap={isAvatarOnlineShown || Boolean(chat.subscriptionUntil)} storyViewerOrigin={StoryViewerOrigin.ChatList} storyViewerMode="single-peer" />
-
+
+ {!isAvatarOnlineShown && Boolean(chat.subscriptionUntil) && ( + + )} = ({ dialogs, currentMessageList }) => { const { dismissDialog, - acceptInviteConfirmation, sendMessage, - showNotification, } = getActions(); const [isModalOpen, openModal, closeModal] = useFlag(); @@ -45,77 +42,6 @@ const Dialogs: FC = ({ dialogs, currentMessageList }) => { return undefined; } - function renderInviteHeader(title: string, photo?: ApiPhoto) { - return ( -
- {photo && } -
- {renderText(title)} -
- -
- ); - } - - const renderInvite = (invite: ApiInviteInfo) => { - const { - hash, title, about, participantsCount, isChannel, photo, isRequestNeeded, - } = invite; - - const handleJoinClick = () => { - acceptInviteConfirmation({ - hash, - }); - if (isRequestNeeded) { - showNotification({ - message: isChannel ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'), - }); - } - closeModal(); - }; - - const participantsText = isChannel - ? lang('Subscribers', participantsCount, 'i') - : lang('Members', participantsCount, 'i'); - - const joinText = isChannel ? lang('ChannelJoin') : lang('JoinGroup'); - const requestToJoinText = isChannel - ? lang('MemberRequests.RequestToJoinChannel') : lang('MemberRequests.RequestToJoinGroup'); - - return ( - - {participantsCount !== undefined &&

{participantsText}

} - {about &&

{renderText(about, ['br'])}

} - {isRequestNeeded && ( -

- {isChannel - ? lang('MemberRequests.RequestToJoinDescriptionChannel') - : lang('MemberRequests.RequestToJoinDescriptionGroup')} -

- )} -
- - -
-
- ); - }; - const renderContactRequest = (contactRequest: ApiContact) => { const handleConfirm = () => { if (!currentMessageList) { @@ -171,11 +97,7 @@ const Dialogs: FC = ({ dialogs, currentMessageList }) => { ); }; - const renderDialog = (dialog: ApiError | ApiInviteInfo | ApiContact) => { - if ('hash' in dialog) { - return renderInvite(dialog); - } - + const renderDialog = (dialog: ApiError | ApiContact) => { if ('phoneNumber' in dialog) { return renderContactRequest(dialog); } diff --git a/src/components/main/premium/StarsGiftModal.async.tsx b/src/components/main/premium/StarsGiftModal.async.tsx index fd9a076ee..064da1c0d 100644 --- a/src/components/main/premium/StarsGiftModal.async.tsx +++ b/src/components/main/premium/StarsGiftModal.async.tsx @@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader'; const StarsGiftModalAsync: FC = (props) => { const { isOpen } = props; - const StarsGiftModal = useModuleLoader(Bundles.Extra, 'StarsGiftModal', !isOpen); + const StarsGiftModal = useModuleLoader(Bundles.Stars, 'StarsGiftModal', !isOpen); // eslint-disable-next-line react/jsx-props-no-spreading return StarsGiftModal ? : undefined; diff --git a/src/components/main/premium/StarsGiftingPickerModal.async.tsx b/src/components/main/premium/StarsGiftingPickerModal.async.tsx index 45e020acf..e097ff3f1 100644 --- a/src/components/main/premium/StarsGiftingPickerModal.async.tsx +++ b/src/components/main/premium/StarsGiftingPickerModal.async.tsx @@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader'; const StarsGiftingPickerModalAsync: FC = (props) => { const { isOpen } = props; - const StarsGiftingPickerModal = useModuleLoader(Bundles.Extra, 'StarsGiftingPickerModal', !isOpen); + const StarsGiftingPickerModal = useModuleLoader(Bundles.Stars, 'StarsGiftingPickerModal', !isOpen); // eslint-disable-next-line react/jsx-props-no-spreading return StarsGiftingPickerModal ? : undefined; diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index c29e08be1..5831a3457 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -8,6 +8,7 @@ import { pick } from '../../util/iteratees'; import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async'; import BoostModal from './boost/BoostModal.async'; +import ChatInviteModal from './chatInvite/ChatInviteModal.async'; import ChatlistModal from './chatlist/ChatlistModal.async'; import CollectibleInfoModal from './collectible/CollectibleInfoModal.async'; import GiftCodeModal from './giftcode/GiftCodeModal.async'; @@ -18,6 +19,7 @@ import PaidReactionModal from './paidReaction/PaidReactionModal.async'; import ReportAdModal from './reportAd/ReportAdModal.async'; import StarsBalanceModal from './stars/StarsBalanceModal.async'; import StarsPaymentModal from './stars/StarsPaymentModal.async'; +import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async'; import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async'; import UrlAuthModal from './urlAuth/UrlAuthModal.async'; import WebAppModal from './webApp/WebAppModal.async'; @@ -39,7 +41,9 @@ type ModalKey = keyof Pick; type StateProps = { @@ -69,7 +73,9 @@ const MODALS: ModalRegistry = { isStarPaymentModalOpen: StarsPaymentModal, starsBalanceModal: StarsBalanceModal, starsTransactionModal: StarsTransactionInfoModal, + chatInviteModal: ChatInviteModal, paidReactionModal: PaidReactionModal, + starsSubscriptionModal: StarsSubscriptionModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/modals/chatInvite/ChatInviteModal.async.tsx b/src/components/modals/chatInvite/ChatInviteModal.async.tsx new file mode 100644 index 000000000..f2845b4e1 --- /dev/null +++ b/src/components/modals/chatInvite/ChatInviteModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './ChatInviteModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const ChatInviteModalAsync: FC = (props) => { + const { modal } = props; + const ChatInviteModal = useModuleLoader(Bundles.Extra, 'ChatInviteModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ChatInviteModal ? : undefined; +}; + +export default ChatInviteModalAsync; diff --git a/src/components/modals/chatInvite/ChatInviteModal.module.scss b/src/components/modals/chatInvite/ChatInviteModal.module.scss new file mode 100644 index 000000000..6ceb0d874 --- /dev/null +++ b/src/components/modals/chatInvite/ChatInviteModal.module.scss @@ -0,0 +1,31 @@ +.content { + display: flex; + flex-direction: column; + align-items: center; +} + +.title { + font-size: 1.5rem; +} + +.participantCount { + color: var(--color-text-secondary); +} + +.participants { + display: flex; + overflow-x: scroll; + gap: 0.5rem; + align-self: stretch; +} + +.participant { + min-width: 4.5rem; + width: 4.5rem; + margin-inline: auto; +} + +.buttons { + align-self: flex-end; + margin-top: 0.5rem; +} diff --git a/src/components/modals/chatInvite/ChatInviteModal.tsx b/src/components/modals/chatInvite/ChatInviteModal.tsx new file mode 100644 index 000000000..bcadcd3f1 --- /dev/null +++ b/src/components/modals/chatInvite/ChatInviteModal.tsx @@ -0,0 +1,108 @@ +import React, { memo, useMemo, useRef } from '../../../lib/teact/teact'; +import { getActions, getGlobal } from '../../../global'; + +import type { TabState } from '../../../global/types'; + +import { getCustomPeerFromInvite, getUserFullName } from '../../../global/helpers'; +import { selectUser } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; + +import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useOldLang from '../../../hooks/useOldLang'; +import usePrevious from '../../../hooks/usePrevious'; + +import Avatar from '../../common/Avatar'; +import FullNameTitle from '../../common/FullNameTitle'; +import PeerBadge from '../../common/PeerBadge'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; + +import styles from './ChatInviteModal.module.scss'; + +export type OwnProps = { + modal: TabState['chatInviteModal']; +}; + +const ChatInviteModal = ({ modal }: OwnProps) => { + const { acceptChatInvite, closeChatInviteModal, showNotification } = getActions(); + // eslint-disable-next-line no-null/no-null + const participantsRef = useRef(null); + + const lang = useOldLang(); + + const prevModal = usePrevious(modal); + const { hash, inviteInfo } = modal || prevModal || {}; + const { + about, isBroadcast, participantIds, participantsCount, photo, isRequestNeeded, + } = inviteInfo || {}; + + const handleClose = useLastCallback(() => { + closeChatInviteModal(); + }); + + const handleAccept = useLastCallback(() => { + acceptChatInvite({ hash: hash! }); + + showNotification({ + message: isBroadcast ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'), + }); + + handleClose(); + }); + + const acceptLangKey = isBroadcast ? 'ProfileJoinChannel' : 'JoinGroup'; + const requestToJoinLangKey = isBroadcast ? 'MemberRequests.RequestToJoinChannel' + : 'MemberRequests.RequestToJoinGroup'; + + const customPeer = useMemo(() => { + if (!inviteInfo) return undefined; + + return getCustomPeerFromInvite(inviteInfo); + }, [inviteInfo]); + + const participants = useMemo(() => { + if (!participantIds) { + return undefined; + } + + const global = getGlobal(); + return participantIds.map((id) => selectUser(global, id)).filter(Boolean); + }, [participantIds]); + + useHorizontalScroll(participantsRef, !modal || !participants); + + return ( + + {customPeer && } + {customPeer && } + {about &&

{about}

} +

+ {lang(isBroadcast ? 'Subscribers' : 'Members', participantsCount, 'i')} +

+ {participants && ( +
+ {participants.map((participant) => ( + + ))} +
+ )} +
+ + +
+
+ ); +}; + +export default memo(ChatInviteModal); diff --git a/src/components/modals/common/TableInfoModal.module.scss b/src/components/modals/common/TableInfoModal.module.scss index 368935990..8dfc1fc25 100644 --- a/src/components/modals/common/TableInfoModal.module.scss +++ b/src/components/modals/common/TableInfoModal.module.scss @@ -13,56 +13,28 @@ } .value { + background-color: var(--color-background); word-break: break-word; + min-width: 2rem; } .table { - border-collapse: separate; - border-spacing: 0; + display: grid; + grid-template-columns: max-content 1fr; + border-radius: 0.3125rem; + border: 1px solid var(--color-borders); + background-color: var(--color-borders); + gap: 1px; + + overflow: hidden; } .cell { - border: solid 0.0625rem var(--color-borders); - border-style: none solid solid none; - padding: 0.25rem 0.5rem; -} - -.row:first-child .cell:first-child { border-top-left-radius: 0.3125rem; } - -.row:first-child .cell:last-child { border-top-right-radius: 0.3125rem; } - -.row:last-child .cell:first-child { border-bottom-left-radius: 0.3125rem; } - -.row:last-child .cell:last-child { border-bottom-right-radius: 0.3125rem; } - -.row:first-child .cell { border-top-style: solid; } - -.row .cell:first-child { border-left-style: solid; } - -.section { display: flex; - flex-direction: column; align-items: center; - - padding: 0.5rem; + padding: 0.25rem 0.5rem; position: relative; - - @include mixins.adapt-padding-to-scrollbar(0.5rem); -} - -.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; + min-height: 2.5rem; } .avatar { diff --git a/src/components/modals/common/TableInfoModal.tsx b/src/components/modals/common/TableInfoModal.tsx index 04a1fcad2..167bdcd5f 100644 --- a/src/components/modals/common/TableInfoModal.tsx +++ b/src/components/modals/common/TableInfoModal.tsx @@ -1,7 +1,7 @@ import React, { memo, type TeactNode } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; -import type { ApiPeer, ApiWebDocument } from '../../../api/types'; +import type { ApiPeer } from '../../../api/types'; import type { CustomPeer } from '../../../types'; import buildClassName from '../../../util/buildClassName'; @@ -15,8 +15,6 @@ import Modal from '../../ui/Modal'; import styles from './TableInfoModal.module.scss'; -import StarsBackground from '../../../assets/stars-bg.png'; - type ChatItem = { chatId: string }; export type TableData = [TeactNode, TeactNode | ChatItem][]; @@ -25,13 +23,7 @@ type OwnProps = { isOpen?: boolean; title?: string; tableData?: TableData; - headerImageUrl?: string; - logoBackground?: string; headerAvatarPeer?: ApiPeer | CustomPeer; - headerAvatarWebPhoto?: ApiWebDocument; - noHeaderImage?: boolean; - isGift?: boolean; - isPrizeStars?: boolean; header?: TeactNode; footer?: TeactNode; buttonText?: string; @@ -44,13 +36,7 @@ const TableInfoModal = ({ isOpen, title, tableData, - headerImageUrl, - logoBackground, headerAvatarPeer, - headerAvatarWebPhoto, - noHeaderImage, - isGift, - isPrizeStars, header, footer, buttonText, @@ -64,8 +50,6 @@ const TableInfoModal = ({ onClose(); }); - const withAvatar = Boolean(headerAvatarPeer || headerAvatarWebPhoto); - return ( - {!isGift && !isPrizeStars && !noHeaderImage && ( - withAvatar ? ( - - ) : ( -
- - {Boolean(logoBackground) - && } -
- ) + {headerAvatarPeer && ( + )} {header} - +
{tableData?.map(([label, value]) => ( -
- - - + + ))} -
{label} + <> +
{label}
+
{typeof value === 'object' && 'chatId' in value ? ( ) : value} -
+
{footer} {buttonText && ( diff --git a/src/components/modals/giftcode/GiftCodeModal.module.scss b/src/components/modals/giftcode/GiftCodeModal.module.scss index 15efed847..022a46fd6 100644 --- a/src/components/modals/giftcode/GiftCodeModal.module.scss +++ b/src/components/modals/giftcode/GiftCodeModal.module.scss @@ -6,3 +6,10 @@ .centered { text-align: center !important; } + +.logo { + width: 7.5rem; + height: 7.5rem; + margin-bottom: 1rem; + align-self: center; +} diff --git a/src/components/modals/giftcode/GiftCodeModal.tsx b/src/components/modals/giftcode/GiftCodeModal.tsx index 58ed565dd..e956c125b 100644 --- a/src/components/modals/giftcode/GiftCodeModal.tsx +++ b/src/components/modals/giftcode/GiftCodeModal.tsx @@ -69,6 +69,7 @@ const GiftCodeModal = ({ const header = ( <> +

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

@@ -110,7 +111,6 @@ const GiftCodeModal = ({ = (props) => { const { modal } = props; - const PaidReactionModal = useModuleLoader(Bundles.Extra, 'PaidReactionModal', !modal); + const PaidReactionModal = useModuleLoader(Bundles.Stars, 'PaidReactionModal', !modal); // eslint-disable-next-line react/jsx-props-no-spreading return PaidReactionModal ? : undefined; diff --git a/src/components/modals/stars/StarsBalanceModal.async.tsx b/src/components/modals/stars/StarsBalanceModal.async.tsx index 3a8f6892f..edb03195f 100644 --- a/src/components/modals/stars/StarsBalanceModal.async.tsx +++ b/src/components/modals/stars/StarsBalanceModal.async.tsx @@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader'; const StarsBalanceModalAsync: FC = (props) => { const { modal } = props; - const StarsBalanceModal = useModuleLoader(Bundles.Extra, 'StarsBalanceModal', !modal); + const StarsBalanceModal = useModuleLoader(Bundles.Stars, 'StarsBalanceModal', !modal); // eslint-disable-next-line react/jsx-props-no-spreading return StarsBalanceModal ? : undefined; diff --git a/src/components/modals/stars/StarsBalanceModal.module.scss b/src/components/modals/stars/StarsBalanceModal.module.scss index a28662b77..943e28487 100644 --- a/src/components/modals/stars/StarsBalanceModal.module.scss +++ b/src/components/modals/stars/StarsBalanceModal.module.scss @@ -42,6 +42,14 @@ @include mixins.side-panel-section; } +.sectionTitle { + color: var(--color-primary); + font-weight: 500; + font-size: 1rem; + align-self: flex-start; + padding: 0.25rem 0.75rem; +} + .secondaryInfo { font-size: 0.875rem; color: var(--color-text-secondary); @@ -76,6 +84,7 @@ margin-inline: 0.5rem; margin-bottom: 1rem; text-wrap: balance; + line-height: 1.375; } .header { @@ -123,10 +132,11 @@ } .balanceBottom { + line-height: 1.5; font-weight: 500; display: flex; + align-items: center; gap: 0.25rem; - line-height: 1.5; } .modalBalance { @@ -162,6 +172,17 @@ z-index: 1; } +.avatarStar { + font-size: 2rem; + + @include mixins.filter-outline(1px, var(--color-background)); + + position: absolute; + right: -1rem; + bottom: 0; + z-index: 1; +} + .paymentImageBackground { height: 7rem; position: absolute; @@ -179,13 +200,27 @@ .paymentButton { display: flex; gap: 0.125rem; + margin-top: 1rem; } .paymentButtonStar { --color-fill: white !important; } -.transactions { +.transactions, .subscriptions { display: flex; flex-direction: column; + width: 100%; +} + +.tabs { + // Disable tabs rounded corners + --border-radius-messages-small: 0; + + top: 3.5rem; +} + +.disclaimer { + margin-top: 0.5rem; + color: var(--color-text-secondary); } diff --git a/src/components/modals/stars/StarsBalanceModal.tsx b/src/components/modals/stars/StarsBalanceModal.tsx index a933f62a7..db43095b8 100644 --- a/src/components/modals/stars/StarsBalanceModal.tsx +++ b/src/components/modals/stars/StarsBalanceModal.tsx @@ -26,7 +26,8 @@ import TabList, { type TabWithProperties } from '../../ui/TabList'; import Transition from '../../ui/Transition'; import BalanceBlock from './BalanceBlock'; import StarTopupOptionList from './StarTopupOptionList'; -import TransactionItem from './transaction/StarsTransactionItem'; +import StarsSubscriptionItem from './subscription/StarsSubscriptionItem'; +import StarsTransactionItem from './transaction/StarsTransactionItem'; import styles from './StarsBalanceModal.module.scss'; @@ -39,6 +40,7 @@ const TRANSACTION_TABS: TabWithProperties[] = [ { title: 'StarsTransactionsIncoming' }, { title: 'StarsTransactionsOutgoing' }, ]; +const TRANSACTION_ITEM_CLASS = 'StarsTransactionItem'; export type OwnProps = { modal: TabState['starsBalanceModal']; @@ -56,7 +58,7 @@ const StarsBalanceModal = ({ closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingModal, openInvoice, } = getActions(); - const { balance, history } = starsBalanceState || {}; + const { balance, history, subscriptions } = starsBalanceState || {}; const oldLang = useOldLang(); const lang = useLang(); @@ -90,13 +92,14 @@ const StarsBalanceModal = ({ return undefined; }, [oldLang, originPayment, originReaction, starsNeeded]); - const shouldShowTransactions = Boolean(history?.all?.transactions.length && !originPayment && !originReaction); + const shouldShowItems = Boolean(history?.all?.transactions.length && !originPayment && !originReaction); const shouldSuggestGifting = !originPayment && !originReaction; useEffect(() => { if (!isOpen) { setHeaderHidden(true); setSelectedTabIndex(0); + hideBuyOptions(); } }, [isOpen]); @@ -127,7 +130,7 @@ const StarsBalanceModal = ({ setHeaderHidden(scrollTop <= 150); } - const handleLoadMore = useLastCallback(() => { + const handleLoadMoreTransactions = useLastCallback(() => { loadStarsTransactions({ type: TRANSACTION_TYPES[selectedTabIndex], }); @@ -170,7 +173,7 @@ const StarsBalanceModal = ({

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

{renderText( @@ -207,7 +210,20 @@ const StarsBalanceModal = ({
{tosText}
- {shouldShowTransactions && ( + {shouldShowItems && Boolean(subscriptions?.list.length) && ( +
+

{oldLang('StarMySubscriptions')}

+
+ {subscriptions?.list.map((subscription) => ( + + ))} +
+
+ )} + {shouldShowItems && (
{history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions.map((transaction) => ( - ))}
= (props) => { const { modal } = props; - const StarPaymentModal = useModuleLoader(Bundles.Extra, 'StarPaymentModal', !modal); + const StarPaymentModal = useModuleLoader(Bundles.Stars, 'StarPaymentModal', !modal); // eslint-disable-next-line react/jsx-props-no-spreading return StarPaymentModal ? : undefined; diff --git a/src/components/modals/stars/StarsPaymentModal.tsx b/src/components/modals/stars/StarsPaymentModal.tsx index b3ac71577..a6c963182 100644 --- a/src/components/modals/stars/StarsPaymentModal.tsx +++ b/src/components/modals/stars/StarsPaymentModal.tsx @@ -2,11 +2,11 @@ import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { - ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiUser, + ApiChat, ApiChatInviteInfo, ApiMediaExtendedPreview, ApiMessage, ApiUser, } from '../../../api/types'; import type { GlobalState, TabState } from '../../../global/types'; -import { getChatTitle, getUserFullName } from '../../../global/helpers'; +import { getChatTitle, getCustomPeerFromInvite, getUserFullName } from '../../../global/helpers'; import { selectChat, selectChatMessage, selectTabState, selectUser, } from '../../../global/selectors'; @@ -14,11 +14,13 @@ 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 useOldLang from '../../../hooks/useOldLang'; import Avatar from '../../common/Avatar'; import StarIcon from '../../common/icons/StarIcon'; +import SafeLink from '../../common/SafeLink'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; import BalanceBlock from './BalanceBlock'; @@ -38,6 +40,7 @@ type StateProps = { bot?: ApiUser; paidMediaMessage?: ApiMessage; paidMediaChat?: ApiChat; + inviteInfo?: ApiChatInviteInfo; }; const StarPaymentModal = ({ @@ -47,6 +50,7 @@ const StarPaymentModal = ({ payment, paidMediaMessage, paidMediaChat, + inviteInfo, }: OwnProps & StateProps) => { const { closePaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions(); const [isLoading, markLoading, unmarkLoading] = useFlag(); @@ -54,7 +58,8 @@ const StarPaymentModal = ({ const photo = payment?.invoice?.photo; - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); useEffect(() => { if (!isOpen) { @@ -68,23 +73,54 @@ const StarPaymentModal = ({ } const botName = getUserFullName(bot); - const starsText = lang('Stars.Intro.PurchasedText.Stars', payment.invoice.amount); + const starsText = oldLang('Stars.Intro.PurchasedText.Stars', payment.invoice.amount); if (paidMediaMessage) { const extendedMedia = paidMediaMessage.content.paidMedia!.extendedMedia as ApiMediaExtendedPreview[]; const areAllPhotos = extendedMedia.every((media) => !media.duration); const areAllVideos = extendedMedia.every((media) => !!media.duration); - const mediaText = areAllPhotos ? lang('Stars.Transfer.Photos', extendedMedia.length) - : areAllVideos ? lang('Stars.Transfer.Videos', extendedMedia.length) - : lang('Media', extendedMedia.length); + const mediaText = areAllPhotos ? oldLang('Stars.Transfer.Photos', extendedMedia.length) + : areAllVideos ? oldLang('Stars.Transfer.Videos', extendedMedia.length) + : oldLang('Media', extendedMedia.length); - const channelTitle = getChatTitle(lang, paidMediaChat!); - return lang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]); + const channelTitle = getChatTitle(oldLang, paidMediaChat!); + return oldLang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]); } - return lang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]); - }, [payment?.invoice, bot, lang, paidMediaMessage, paidMediaChat]); + if (inviteInfo) { + return lang('StarsSubscribeText', { + chat: inviteInfo.title, + amount: payment.invoice.amount, + }, { + withNodes: true, + withMarkdown: true, + pluralValue: payment.invoice.amount, + }); + } + + return oldLang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]); + }, [payment?.invoice, bot, oldLang, lang, paidMediaMessage, paidMediaChat, inviteInfo]); + + const disclaimerText = useMemo(() => { + if (inviteInfo) { + return lang('StarsSubscribeInfo', { + link: , + }, { + withNodes: true, + }); + } + + return undefined; + }, [inviteInfo, lang]); + + const inviteCustomPeer = useMemo(() => { + if (!inviteInfo) { + return undefined; + } + + return getCustomPeerFromInvite(inviteInfo); + }, [inviteInfo]); const handlePayment = useLastCallback(() => { const price = payment?.invoice?.amount; @@ -113,9 +149,14 @@ const StarPaymentModal = ({ onClose={closePaymentModal} > -
+
{paidMediaMessage ? ( + ) : inviteCustomPeer ? ( + <> + + + ) : ( <> @@ -125,18 +166,23 @@ const StarPaymentModal = ({

- {lang('StarsConfirmPurchaseTitle')} + {inviteCustomPeer ? oldLang('StarsSubscribeTitle') : oldLang('StarsConfirmPurchaseTitle')}

-
+
{renderText(descriptionText, ['simple_markdown', 'emoji'])}
+ {disclaimerText && ( +
+ {disclaimerText} +
+ )} ); }; @@ -152,12 +198,17 @@ export default memo(withGlobal( const chat = messageInputInvoice ? selectChat(global, messageInputInvoice.chatId) : undefined; const isPaidMedia = message?.content.paidMedia; + const inviteInputInvoice = payment.inputInvoice?.type === 'chatInviteSubscription' + ? payment.inputInvoice : undefined; + const inviteInfo = inviteInputInvoice?.inviteInfo; + return { bot, starsBalanceState: global.stars, payment, paidMediaMessage: isPaidMedia ? message : undefined, paidMediaChat: isPaidMedia ? chat : undefined, + inviteInfo, }; }, )(StarPaymentModal)); diff --git a/src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss b/src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss new file mode 100644 index 000000000..aa39c6ec7 --- /dev/null +++ b/src/components/modals/stars/subscription/StarsSubscriptionItem.module.scss @@ -0,0 +1,69 @@ +@use '../../../../styles/mixins'; + +.root { + display: flex; + gap: 0.75rem; + padding: 0.25rem 0.75rem 0.25rem 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; +} + +.status { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; +} + +.statusPricing { + display: flex; + align-items: center; + flex-shrink: 0; + gap: 0.25rem; +} + +.amount { + font-weight: 500; +} + +.title, .description { + margin-bottom: 0; +} + +.title { + font-size: 1rem; +} + +.description, .statusPeriod, .statusEnded { + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.statusEnded { + color: var(--color-error); +} + +.preview { + position: relative; + align-self: flex-start; +} + +.subscriptionStar { + position: absolute; + right: 0; + bottom: 0; + z-index: 1; + + @include mixins.filter-outline(1px, var(--color-background)); +} diff --git a/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx b/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx new file mode 100644 index 000000000..3e80809c0 --- /dev/null +++ b/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx @@ -0,0 +1,89 @@ +import React, { memo } from '../../../../lib/teact/teact'; +import { getActions } from '../../../../global'; + +import type { + ApiStarsSubscription, +} from '../../../../api/types'; +import type { GlobalState } from '../../../../global/types'; + +import { getSenderTitle } from '../../../../global/helpers'; +import { selectPeer } from '../../../../global/selectors'; +import { formatDateToString } from '../../../../util/dates/dateFormat'; +import { formatInteger } from '../../../../util/textFormat'; + +import useSelector from '../../../../hooks/data/useSelector'; +import useLastCallback from '../../../../hooks/useLastCallback'; +import useOldLang from '../../../../hooks/useOldLang'; + +import Avatar from '../../../common/Avatar'; +import StarIcon from '../../../common/icons/StarIcon'; + +import styles from './StarsSubscriptionItem.module.scss'; + +type OwnProps = { + subscription: ApiStarsSubscription; +}; + +function selectProvidedPeer(peerId: string) { + return (global: GlobalState) => ( + selectPeer(global, peerId) + ); +} + +const StarsSubscriptionItem = ({ subscription }: OwnProps) => { + const { openStarsSubscriptionModal } = getActions(); + const { + peerId, pricing, until, isCancelled, + } = subscription; + const lang = useOldLang(); + + const peer = useSelector(selectProvidedPeer(peerId))!; + + const handleClick = useLastCallback(() => { + openStarsSubscriptionModal({ subscription }); + }); + + if (!peer) { + return undefined; + } + const hasExpired = until < Date.now() / 1000; + const formattedDate = formatDateToString(until * 1000, lang.code, true, 'long'); + + return ( +
+
+ + +
+
+

{getSenderTitle(lang, peer)}

+

+ {lang( + hasExpired ? 'StarsSubscriptionExpired' + : isCancelled ? 'StarsSubscriptionExpires' : 'StarsSubscriptionRenews', + formattedDate, + )} +

+
+
+ {(isCancelled || hasExpired) ? ( +
+ {lang(hasExpired ? 'StarsSubscriptionStatusExpired' : 'StarsSubscriptionStatusCancelled')} +
+ ) : ( + <> +
+ + + {formatInteger(pricing.amount)} + +
+
{lang('StarsParticipantSubscriptionPerMonth')}
+ + )} +
+
+ ); +}; + +export default memo(StarsSubscriptionItem); diff --git a/src/components/modals/stars/subscription/StarsSubscriptionModal.async.tsx b/src/components/modals/stars/subscription/StarsSubscriptionModal.async.tsx new file mode 100644 index 000000000..126e6181a --- /dev/null +++ b/src/components/modals/stars/subscription/StarsSubscriptionModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../../lib/teact/teact'; +import React from '../../../../lib/teact/teact'; + +import type { OwnProps } from './StarsSubscriptionModal'; + +import { Bundles } from '../../../../util/moduleLoader'; + +import useModuleLoader from '../../../../hooks/useModuleLoader'; + +const StarsSubscriptionModalAsync: FC = (props) => { + const { modal } = props; + const StarsSubscriptionModal = useModuleLoader(Bundles.Stars, 'StarsSubscriptionModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return StarsSubscriptionModal ? : undefined; +}; + +export default StarsSubscriptionModalAsync; diff --git a/src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss b/src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss new file mode 100644 index 000000000..c3705a040 --- /dev/null +++ b/src/components/modals/stars/subscription/StarsSubscriptionModal.module.scss @@ -0,0 +1,72 @@ +@use '../../../../styles/mixins'; + +.modal { + z-index: calc(var(--z-modal-low-priority) + 1); +} + +.header { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + position: relative; +} + +.starsHeader { + gap: normal; +} + +.title, .amount { + margin-bottom: 0; +} + +.amount { + display: flex; + align-items: center; + text-align: center; + color: var(--color-text-secondary); +} + +.footer { + text-align: center; + margin-block: 0.5rem; + color: var(--color-text-secondary); + font-size: 0.875rem; +} + +.starsBackground { + position: absolute; + height: 8rem; + top: 0; + left: 50%; + transform: translateX(-50%); + z-index: -1; +} + +.avatarWrapper { + position: relative; +} + +.subscriptionStar { + position: absolute; + bottom: 0; + right: 0; + + font-size: 2rem; + z-index: 1; + + @include mixins.filter-outline(2px, var(--color-background)); +} + +.amountStar { + font-size: 1.25rem; +} + +.secondary { + color: var(--color-text-secondary); +} + +.danger { + color: var(--color-error); +} diff --git a/src/components/modals/stars/subscription/StarsSubscriptionModal.tsx b/src/components/modals/stars/subscription/StarsSubscriptionModal.tsx new file mode 100644 index 000000000..907a2fed8 --- /dev/null +++ b/src/components/modals/stars/subscription/StarsSubscriptionModal.tsx @@ -0,0 +1,230 @@ +import type { FC } from '../../../../lib/teact/teact'; +import React, { memo, useMemo } from '../../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../../global'; + +import type { + ApiPeer, +} from '../../../../api/types'; +import type { TabState } from '../../../../global/types'; + +import { + selectPeer, +} from '../../../../global/selectors'; +import buildClassName from '../../../../util/buildClassName'; +import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; + +import useLang from '../../../../hooks/useLang'; +import useLastCallback from '../../../../hooks/useLastCallback'; +import useOldLang from '../../../../hooks/useOldLang'; +import usePrevious from '../../../../hooks/usePrevious'; + +import Avatar from '../../../common/Avatar'; +import StarIcon from '../../../common/icons/StarIcon'; +import SafeLink from '../../../common/SafeLink'; +import Button from '../../../ui/Button'; +import TableInfoModal, { type TableData } from '../../common/TableInfoModal'; + +import styles from './StarsSubscriptionModal.module.scss'; + +import StarsBackground from '../../../../assets/stars-bg.png'; + +export type OwnProps = { + modal: TabState['starsSubscriptionModal']; +}; + +type StateProps = { + peer?: ApiPeer; +}; + +const StarsSubscriptionModal: FC = ({ + modal, peer, +}) => { + const { + closeStarsSubscriptionModal, + fulfillStarsSubscription, + changeStarsSubscription, + checkChatInvite, + loadStarStatus, + } = getActions(); + const oldLang = useOldLang(); + const lang = useLang(); + const { subscription } = modal || {}; + + const buttonState = useMemo(() => { + if (!subscription) { + return 'hidden'; + } + + if (subscription.canRefulfill) { + return 'refulfill'; + } + + const isActive = subscription.until > Date.now() / 1000; + if (isActive && !subscription.isCancelled) { + return 'cancel'; + } + + if (isActive && subscription.isCancelled) { + return 'renew'; + } + + if (!isActive) { + return 'restart'; + } + + return 'ok'; + }, [subscription]); + + const handleButtonClick = useLastCallback(() => { + if (!subscription) { + return; + } + + switch (buttonState) { + case 'refulfill': { + fulfillStarsSubscription({ id: subscription.id }); + break; + } + case 'restart': { + checkChatInvite({ hash: subscription.chatInviteHash! }); + loadStarStatus(); + break; + } + case 'renew': { + changeStarsSubscription({ id: subscription.id, isCancelled: false }); + break; + } + case 'cancel': { + changeStarsSubscription({ id: subscription.id, isCancelled: true }); + break; + } + } + closeStarsSubscriptionModal(); + }); + + const starModalData = useMemo(() => { + if (!subscription || !peer) { + return undefined; + } + + const { + pricing, until, isCancelled, canRefulfill, + } = subscription; + + const header = ( +
+
+ + +
+ +

{oldLang('StarsSubscriptionTitle')}

+

+ {lang('StarsPerMonth', { + amount: pricing.amount, + }, { + withNodes: true, + specialReplacement: { + '⭐️': , + }, + })} +

+
+ ); + + const tableData: TableData = []; + + tableData.push([ + oldLang('StarsSubscriptionChannel'), + { chatId: peer.id }, + ]); + + const hasExpired = until < Date.now() / 1000; + tableData.push([ + oldLang(hasExpired ? 'StarsSubscriptionUntilExpired' + : isCancelled ? 'StarsSubscriptionUntilExpires' : 'StarsSubscriptionUntilRenews'), + formatDateTimeToString(until * 1000, oldLang.code, true), + ]); + + const footerTos = lang('StarsTransactionTOS', { + link: , + }, { + withNodes: true, + }); + + const footer = ( + +

{footerTos}

+ {isCancelled && ( +

{oldLang('StarsSubscriptionCancelledText')}

+ )} + {canRefulfill && ( +

+ {oldLang('StarsSubscriptionRefulfillInfo', formatDateTimeToString(until * 1000, oldLang.code, true))} +

+ )} + {!isCancelled && !canRefulfill && hasExpired && ( +

+ {oldLang('StarsSubscriptionExpiredInfo', formatDateTimeToString(until * 1000, oldLang.code, true))} +

+ )} + {!isCancelled && !canRefulfill && !hasExpired && ( +

+ {oldLang('StarsSubscriptionCancelInfo', formatDateTimeToString(until * 1000, oldLang.code, true))} +

+ )} + {buttonState !== 'hidden' && ( + + )} +
+ ); + + return { + header, + tableData, + footer, + }; + }, [buttonState, lang, oldLang, peer, subscription]); + + const prevModalData = usePrevious(starModalData); + const renderingModalData = prevModalData || starModalData; + + return ( + + ); +}; + +export default memo(withGlobal( + (global, { modal }): StateProps => { + const peerId = modal?.subscription.peerId; + const peer = peerId ? selectPeer(global, peerId) : undefined; + + return { + peer, + }; + }, +)(StarsSubscriptionModal)); diff --git a/src/components/modals/stars/transaction/StarsTransactionItem.module.scss b/src/components/modals/stars/transaction/StarsTransactionItem.module.scss index 34c91d929..4a8cec3e6 100644 --- a/src/components/modals/stars/transaction/StarsTransactionItem.module.scss +++ b/src/components/modals/stars/transaction/StarsTransactionItem.module.scss @@ -1,7 +1,9 @@ +@use '../../../../styles/mixins'; + .root { display: flex; gap: 0.75rem; - padding: 0.25rem; + padding: 0.25rem 0.75rem 0.25rem 0.25rem; border-radius: 0.5rem; cursor: var(--custom-cursor, pointer); transition: background-color 0.25s ease-out; @@ -28,10 +30,6 @@ font-weight: 500; } -.star { - margin-top: -0.25rem; -} - .title, .description, .date { margin-bottom: 0; } @@ -56,3 +54,17 @@ .negative { color: var(--color-error); } + +.preview { + position: relative; + align-self: flex-start; +} + +.subscriptionStar { + position: absolute; + right: 0; + bottom: 0; + z-index: 1; + + @include mixins.filter-outline(1px, var(--color-background)); +} diff --git a/src/components/modals/stars/transaction/StarsTransactionItem.tsx b/src/components/modals/stars/transaction/StarsTransactionItem.tsx index fa1b8052d..415b94640 100644 --- a/src/components/modals/stars/transaction/StarsTransactionItem.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionItem.tsx @@ -27,6 +27,7 @@ import styles from './StarsTransactionItem.module.scss'; type OwnProps = { transaction: ApiStarsTransaction; + className?: string; }; function selectOptionalPeer(peerId?: string) { @@ -35,7 +36,7 @@ function selectOptionalPeer(peerId?: string) { ); } -const StarsTransactionItem = ({ transaction }: OwnProps) => { +const StarsTransactionItem = ({ transaction, className }: OwnProps) => { const { openStarsTransactionModal } = getActions(); const { date, @@ -43,6 +44,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => { photo, peer: transactionPeer, extendedMedia, + subscriptionPeriod, } = transaction; const lang = useOldLang(); @@ -50,15 +52,13 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => { const peer = useSelector(selectOptionalPeer(peerId)); const data = useMemo(() => { - let title = transaction.title; - if (transaction.extendedMedia) { - title = lang('StarMediaPurchase'); - } - - if (transaction.isReaction) { - title = lang('StarsReactionsSent'); - } + let title = (() => { + if (transaction.extendedMedia) return lang('StarMediaPurchase'); + if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase'); + if (transaction.isReaction) return lang('StarsReactionsSent'); + return transaction.title; + })(); let description; let status: string | undefined; let avatarPeer: ApiPeer | CustomPeer | undefined; @@ -68,7 +68,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => { avatarPeer = peer || CUSTOM_PEER_PREMIUM; } else { const customPeer = buildStarsTransactionCustomPeer(transaction.peer); - title = lang(customPeer.titleKey); + title = customPeer.title || lang(customPeer.titleKey!); description = lang(customPeer.subtitleKey!); avatarPeer = customPeer; } @@ -102,9 +102,14 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => { }); return ( -
- {extendedMedia ? - : } +
+
+ {extendedMedia ? + : } + {Boolean(subscriptionPeriod) && ( + + )} +

{data.title}

{data.description}

@@ -117,7 +122,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => { {formatStarsTransactionAmount(stars)} - +
); diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.async.tsx b/src/components/modals/stars/transaction/StarsTransactionModal.async.tsx index 9aeeafca7..f1c1f8547 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.async.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionModal.async.tsx @@ -9,7 +9,7 @@ import useModuleLoader from '../../../../hooks/useModuleLoader'; const StarsTransactionModalAsync: FC = (props) => { const { modal } = props; - const StarsTransactionModal = useModuleLoader(Bundles.Extra, 'StarsTransactionInfoModal', !modal); + const StarsTransactionModal = useModuleLoader(Bundles.Stars, 'StarsTransactionInfoModal', !modal); // eslint-disable-next-line react/jsx-props-no-spreading return StarsTransactionModal ? : undefined; diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.module.scss b/src/components/modals/stars/transaction/StarsTransactionModal.module.scss index 38d2e8797..f69cfe9cb 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.module.scss +++ b/src/components/modals/stars/transaction/StarsTransactionModal.module.scss @@ -1,11 +1,9 @@ +@use "../../../../styles/mixins"; + .modal { z-index: calc(var(--z-modal-low-priority) + 1); } -.modal :global(.modal-dialog) { - max-width: 26rem !important; -} - .positive { color: var(--color-success); } @@ -43,6 +41,11 @@ font-family: var(--font-family-monospace); font-size: 0.875rem; cursor: pointer; + + overflow: hidden; + white-space: nowrap; + + @include mixins.gradient-border-right(3rem, 1rem); } .description { @@ -53,27 +56,31 @@ text-align: center; margin-block: 0.5rem; color: var(--color-text-secondary); + font-size: 0.875rem; } .starsBackground { position: absolute; height: 8rem; - top: -8.5rem; + top: 0; left: 50%; transform: translateX(-50%); -} - -.mediaShift { - top: -1.5rem; + z-index: -1; } .copyIcon { + position: absolute; + right: 0.25rem; + top: 50%; + transform: translateY(-50%); + margin-inline-start: 0.25rem; color: var(--color-primary); + pointer-events: none; } .mediaPreview { - margin-bottom: 2rem; + margin-block: 1.5rem 1rem; cursor: var(--custom-cursor, pointer); } diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.tsx b/src/components/modals/stars/transaction/StarsTransactionModal.tsx index bdcf81216..f76d9122d 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionModal.tsx @@ -24,9 +24,10 @@ import renderText from '../../../common/helpers/renderText'; import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; import useOldLang from '../../../../hooks/useOldLang'; -import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated'; +import usePrevious from '../../../../hooks/usePrevious'; import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker'; +import Avatar from '../../../common/Avatar'; import Icon from '../../../common/icons/Icon'; import StarIcon from '../../../common/icons/StarIcon'; import SafeLink from '../../../common/SafeLink'; @@ -55,8 +56,6 @@ const StarsTransactionModal: FC = ({ const oldLang = useOldLang(); const lang = useLang(); const { transaction } = modal || {}; - const isGift = transaction?.isGift; - const isPrizeStars = transaction?.isPrizeStars; const handleOpenMedia = useLastCallback(() => { const media = transaction?.extendedMedia; @@ -68,22 +67,6 @@ const StarsTransactionModal: FC = ({ }); }); - const animatedStickerData = useMemo(() => { - if (!transaction) { - return undefined; - } - - return ( - - ); - }, [canPlayAnimatedEmojis, starGiftSticker, transaction]); - const giftEntryAboutText = useMemo(() => { const subtitleText = oldLang('lng_credits_box_history_entry_gift_in_about'); const subtitleTextParts = subtitleText.split('{link}'); @@ -127,24 +110,23 @@ const StarsTransactionModal: FC = ({ return undefined; } + const { isGift, isPrizeStars, photo } = transaction; + const customPeer = (transaction.peer && transaction.peer.type !== 'peer' && buildStarsTransactionCustomPeer(transaction.peer)) || undefined; const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined; const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer)); - let title = transaction.title; - if (!title && customPeer) { - title = oldLang(customPeer.titleKey); - } + const title = (() => { + if (transaction.extendedMedia) return oldLang('StarMediaPurchase'); + if (transaction.subscriptionPeriod) return oldLang('StarSubscriptionPurchase'); + if (transaction.isReaction) return oldLang('StarsReactionsSent'); - if (!title && transaction.extendedMedia) { - title = oldLang('StarMediaPurchase'); - } + if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!); - if (!title && transaction.isReaction) { - title = oldLang('StarsReactionsSent'); - } + return transaction.title; + })(); const messageLink = peer && transaction.messageId ? getMessageLink(peer, undefined, transaction.messageId) : undefined; @@ -161,6 +143,9 @@ const StarsTransactionModal: FC = ({ const description = transaction.description || (media ? mediaText : undefined); + const shouldDisplayAvatar = !media && !isGift && !isPrizeStars; + const avatarPeer = !photo ? (peer || customPeer) : undefined; + const header = (
{media && ( @@ -170,14 +155,24 @@ const StarsTransactionModal: FC = ({ onClick={handleOpenMedia} /> )} - {(isGift || isPrizeStars) ? animatedStickerData : ( - )} + {shouldDisplayAvatar && ( + + )} + {title &&

{title}

} {(isGift || isPrizeStars) && (

@@ -223,18 +218,20 @@ const StarsTransactionModal: FC = ({ tableData.push([ oldLang('Stars.Transaction.Id'), ( - { - copyTextToClipboard(transaction.id!); - showNotification({ - message: oldLang('StarsTransactionIDCopied'), - }); - }} - > - {transaction.id} + <> +
{ + copyTextToClipboard(transaction.id!); + showNotification({ + message: oldLang('StarsTransactionIDCopied'), + }); + }} + > + {transaction.id} +
-
+ ), ]); } @@ -259,13 +256,12 @@ const StarsTransactionModal: FC = ({ header, tableData, footer, - avatarPeer: !transaction.photo ? (peer || customPeer) : undefined, }; }, [ - transaction, oldLang, peer, isGift, isPrizeStars, animatedStickerData, giftOutAboutText, giftEntryAboutText, + transaction, oldLang, peer, giftOutAboutText, giftEntryAboutText, canPlayAnimatedEmojis, starGiftSticker, ]); - const prevModalData = usePreviousDeprecated(starModalData); + const prevModalData = usePrevious(starModalData); const renderingModalData = prevModalData || starModalData; return ( @@ -273,13 +269,8 @@ const StarsTransactionModal: FC = ({ isOpen={Boolean(transaction)} className={styles.modal} header={renderingModalData?.header} - isGift={isGift} - isPrizeStars={isPrizeStars} tableData={renderingModalData?.tableData} footer={renderingModalData?.footer} - noHeaderImage={Boolean(transaction?.extendedMedia)} - headerAvatarWebPhoto={transaction?.photo} - headerAvatarPeer={renderingModalData?.avatarPeer} buttonText={oldLang('OK')} onClose={closeStarsTransactionModal} /> diff --git a/src/components/right/statistics/BoostStatistics.tsx b/src/components/right/statistics/BoostStatistics.tsx index 51c3eb78f..763510696 100644 --- a/src/components/right/statistics/BoostStatistics.tsx +++ b/src/components/right/statistics/BoostStatistics.tsx @@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiBoost, ApiBoostStatistics, ApiTypePrepaidGiveaway } from '../../../api/types'; import type { TabState } from '../../../global/types'; +import type { CustomPeer } from '../../../types'; import { GIVEAWAY_BOOST_PER_PREMIUM, @@ -17,7 +18,6 @@ import { } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { formatDateAtTime } from '../../../util/dates/dateFormat'; -import { CUSTOM_PEER_STAR, CUSTOM_PEER_TO_BE_DISTRIBUTED } from '../../../util/objects/customPeer'; import { formatInteger } from '../../../util/textFormat'; import { getBoostProgressInfo } from '../../common/helpers/boostInfo'; @@ -56,6 +56,19 @@ const GIVEAWAY_IMG_LIST: { [key: number]: string } = { 12: GiftRedRound, }; +const CUSTOM_PEER_STAR_TEMPLATE: Omit = { + isCustomPeer: true, + avatarIcon: 'star', + peerColorId: 1, +}; + +const CUSTOM_PEER_TO_BE_DISTRIBUTED: CustomPeer = { + isCustomPeer: true, + titleKey: 'BoostingToBeDistributed', + avatarIcon: 'user', + withPremiumGradient: true, +}; + const BoostStatistics = ({ boostStatistics, isGiveawayAvailable, @@ -199,6 +212,18 @@ const BoostStatistics = ({ const renderBoostList = useLastCallback((boost) => { const hasStars = Boolean(boost?.stars); + let customPeer: CustomPeer | undefined; + if (hasStars) { + customPeer = { + ...CUSTOM_PEER_STAR_TEMPLATE, + title: lang('Stars', boost.stars), + }; + } + + if (!boost.userId) { + customPeer = CUSTOM_PEER_TO_BE_DISTRIBUTED; + } + return ( void; onScroll?: (e: UIEvent) => void; @@ -61,6 +62,7 @@ const InfiniteScroll: FC = ({ cacheBuster, beforeChildren, children, + scrollContainerClosest, onLoadMore, onScroll, onWheel, @@ -100,8 +102,10 @@ const InfiniteScroll: FC = ({ // Initial preload useEffect(() => { - const container = containerRef.current; - if (!loadMoreBackwards || !container) { + const scrollContainer = scrollContainerClosest + ? containerRef.current!.closest(scrollContainerClosest)! + : containerRef.current!; + if (!loadMoreBackwards || !scrollContainer) { return; } @@ -110,16 +114,21 @@ const InfiniteScroll: FC = ({ return; } - const { scrollHeight, clientHeight } = container; + const { scrollHeight, clientHeight } = scrollContainer; if (clientHeight && scrollHeight < clientHeight) { loadMoreBackwards(); } - }, [items, loadMoreBackwards, preloadBackwards]); + }, [items, loadMoreBackwards, preloadBackwards, scrollContainerClosest]); // Restore `scrollTop` after adding items useLayoutEffect(() => { + const scrollContainer = scrollContainerClosest + ? containerRef.current!.closest(scrollContainerClosest)! + : containerRef.current!; + + const container = containerRef.current!; + requestForcedReflow(() => { - const container = containerRef.current!; const state = stateRef.current; state.listItemElements = container.querySelectorAll(itemSelector); @@ -127,7 +136,7 @@ const InfiniteScroll: FC = ({ let newScrollTop: number; if (state.currentAnchor && Array.from(state.listItemElements).includes(state.currentAnchor)) { - const { scrollTop } = container; + const { scrollTop } = scrollContainer; const newAnchorTop = state.currentAnchor!.getBoundingClientRect().top; newScrollTop = scrollTop + (newAnchorTop - state.currentAnchorTop!); } else { @@ -142,18 +151,21 @@ const InfiniteScroll: FC = ({ return undefined; } - const { scrollTop } = container; + const { scrollTop } = scrollContainer; if (noScrollRestoreOnTop && scrollTop === 0) { return undefined; } return () => { - resetScroll(container, newScrollTop); + resetScroll(scrollContainer, newScrollTop); state.isScrollTopJustUpdated = true; }; }); - }, [items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning]); + }, [ + items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning, + scrollContainerClosest, + ]); const handleScroll = useLastCallback((e: UIEvent) => { if (loadMoreForwards && loadMoreBackwards) { @@ -168,8 +180,10 @@ const InfiniteScroll: FC = ({ } const listLength = listItemElements.length; - const container = containerRef.current!; - const { scrollTop, scrollHeight, offsetHeight } = container; + const scrollContainer = scrollContainerClosest + ? containerRef.current!.closest(scrollContainerClosest)! + : containerRef.current!; + const { scrollTop, scrollHeight, offsetHeight } = scrollContainer; const top = listLength ? listItemElements[0].offsetTop : 0; const isNearTop = scrollTop <= top + sensitiveArea; const bottom = listLength @@ -237,11 +251,25 @@ const InfiniteScroll: FC = ({ } }); + useLayoutEffect(() => { + const scrollContainer = scrollContainerClosest + ? containerRef.current!.closest(scrollContainerClosest)! + : containerRef.current!; + if (!scrollContainer) return undefined; + + const handleNativeScroll = (e: Event) => handleScroll(e as unknown as UIEvent); + + scrollContainer.addEventListener('scroll', handleNativeScroll); + + return () => { + scrollContainer.removeEventListener('scroll', handleNativeScroll); + }; + }, [handleScroll, scrollContainerClosest]); + return (
= }); addActionHandler('openLinkedChat', async (global, actions, payload): Promise => { - const { id, tabId = getCurrentTabId() } = payload!; + const { id, tabId = getCurrentTabId() } = payload; const chat = selectChat(global, id); if (!chat) { return; @@ -534,7 +537,7 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => { const { chatId, force, withPhotos, - } = payload!; + } = payload; const chat = selectChat(global, chatId); if (!chat) { return; @@ -716,7 +719,7 @@ addActionHandler('createChannel', async (global, actions, payload): Promise => { - const { chatId, tabId = getCurrentTabId() } = payload!; + const { chatId, tabId = getCurrentTabId() } = payload; const chat = selectChat(global, chatId); if (!chat) { return; @@ -758,7 +761,7 @@ addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType }); addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => { - const { chatId, tabId = getCurrentTabId() } = payload!; + const { chatId, tabId = getCurrentTabId() } = payload; const chat = selectChat(global, chatId); if (!chat) { return; @@ -775,7 +778,7 @@ addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => { }); addActionHandler('leaveChannel', async (global, actions, payload): Promise => { - const { chatId, tabId = getCurrentTabId() } = payload!; + const { chatId, tabId = getCurrentTabId() } = payload; const chat = selectChat(global, chatId); if (!chat) { return; @@ -797,8 +800,6 @@ addActionHandler('leaveChannel', async (global, actions, payload): Promise global = deleteChatMessages(global, chatId, localMessageIds); setGlobal(global); } - - actions.loadFullChat({ chatId, force: true }); }); addActionHandler('deleteChannel', (global, actions, payload): ActionReturnType => { @@ -891,7 +892,7 @@ addActionHandler('createGroupChat', async (global, actions, payload): Promise { - const { id, folderId, tabId = getCurrentTabId() } = payload!; + const { id, folderId, tabId = getCurrentTabId() } = payload; const chat = selectChat(global, id); if (!chat) { return; @@ -938,7 +939,7 @@ addActionHandler('toggleChatPinned', (global, actions, payload): ActionReturnTyp }); addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnType => { - const { id } = payload!; + const { id } = payload; const chat = selectChat(global, id); if (chat) { void callApi('toggleChatArchived', { @@ -949,7 +950,7 @@ addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnT }); addActionHandler('toggleSavedDialogPinned', (global, actions, payload): ActionReturnType => { - const { id, tabId = getCurrentTabId() } = payload!; + const { id, tabId = getCurrentTabId() } = payload; const chat = selectChat(global, id); if (!chat) { return; @@ -1046,7 +1047,7 @@ addActionHandler('editChatFolders', (global, actions, payload): ActionReturnType }); addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType => { - const { id, folderUpdate } = payload!; + const { id, folderUpdate } = payload; const folder = selectChatFolder(global, id); if (folder) { @@ -1063,7 +1064,7 @@ addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType }); addActionHandler('addChatFolder', async (global, actions, payload): Promise => { - const { folder, tabId = getCurrentTabId() } = payload!; + const { folder, tabId = getCurrentTabId() } = payload; const { orderedIds, byId } = global.chatFolders; const limit = selectCurrentLimit(global, 'dialogFilters'); @@ -1125,7 +1126,7 @@ addActionHandler('addChatFolder', async (global, actions, payload): Promise => { - const { folderIds } = payload!; + const { folderIds } = payload; const result = await callApi('sortChatFolders', folderIds); if (result) { @@ -1191,21 +1192,65 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType = setGlobal(global); }); -addActionHandler('openChatByInvite', async (global, actions, payload): Promise => { - const { hash, tabId = getCurrentTabId() } = payload!; +addActionHandler('checkChatInvite', async (global, actions, payload): Promise => { + const { hash, tabId = getCurrentTabId() } = payload; - const result = await callApi('openChatByInvite', hash); + const result = await callApi('checkChatInvite', hash); if (!result) { return; } - actions.openChat({ id: result.chatId, tabId }); + global = getGlobal(); + + if (result.users) { + global = addUsers(global, buildCollectionByKey(result.users, 'id')); + } + + if (result.chat) { + global = addChats(global, buildCollectionByKey([result.chat], 'id')); + setGlobal(global); + actions.openChat({ id: result.chat.id, tabId }); + return; + } + + if (result.invite.subscriptionFormId) { + global = updateTabState(global, { + payment: { + formId: result.invite.subscriptionFormId, + inputInvoice: { + type: 'chatInviteSubscription', + hash, + inviteInfo: result.invite, + }, + invoice: { + amount: result.invite.subscriptionPricing!.amount, + currency: STARS_CURRENCY_CODE, + isRecurring: true, + mediaType: 'invoice', + // Placeholder values + title: 'Subscription', + text: '', + }, + }, + isStarPaymentModalOpen: true, + }, tabId); + setGlobal(global); + return; + } + + global = updateTabState(global, { + chatInviteModal: { + hash, + inviteInfo: result.invite, + }, + }, tabId); + setGlobal(global); }); addActionHandler('openChatByPhoneNumber', async (global, actions, payload): Promise => { const { phoneNumber, startAttach, attach, text, tabId = getCurrentTabId(), - } = payload!; + } = payload; // Open temporary empty chat to make the click response feel faster actions.openChat({ id: TMP_CHAT_ID, tabId }); @@ -1240,7 +1285,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise => { - const { hash, tabId = getCurrentTabId() } = payload!; +addActionHandler('acceptChatInvite', async (global, actions, payload): Promise => { + const { hash, tabId = getCurrentTabId() } = payload; const result = await callApi('importChatInvite', { hash }); if (!result) { return; @@ -1467,7 +1512,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise const { username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, text, tabId = getCurrentTabId(), - } = payload!; + } = payload; const chat = selectCurrentChat(global, tabId); const webAppName = originalParts?.[1]; @@ -1557,7 +1602,7 @@ addActionHandler('togglePreHistoryHidden', async (global, actions, payload): Pro const { chatId, isEnabled, tabId = getCurrentTabId(), - } = payload!; + } = payload; const chat = await ensureIsSuperGroup(global, actions, chatId, tabId); if (!chat) { @@ -1572,7 +1617,7 @@ addActionHandler('togglePreHistoryHidden', async (global, actions, payload): Pro }); addActionHandler('updateChatDefaultBannedRights', (global, actions, payload): ActionReturnType => { - const { chatId, bannedRights } = payload!; + const { chatId, bannedRights } = payload; const chat = selectChat(global, chatId); if (!chat) { @@ -1586,7 +1631,7 @@ addActionHandler('updateChatMemberBannedRights', async (global, actions, payload const { chatId, userId, bannedRights, tabId = getCurrentTabId(), - } = payload!; + } = payload; const user = selectUser(global, userId); @@ -1634,7 +1679,7 @@ addActionHandler('updateChatAdmin', async (global, actions, payload): Promise { - const { - chatId, - messageId, - tabId = getCurrentTabId(), - } = payload || {}; - - const message = selectChatMessage(global, chatId, messageId); - if (!message) return undefined; - - const transaction = getStarsTransactionFromGift(message); - if (!transaction) return undefined; - - return openStarsTransactionModal(global, transaction, tabId); -}); - addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => { const { chatId, @@ -1056,11 +1045,18 @@ addActionHandler('loadStarStatus', async (global): Promise => { inbound: undefined, outbound: undefined, }, + subscriptions: undefined, }, }; + if (status.history) { - global = appendStarsTransactions(global, 'all', status.history, status.nextOffset); + global = appendStarsTransactions(global, 'all', status.history, status.nextHistoryOffset); } + + if (status.subscriptions) { + global = appendStarsSubscriptions(global, status.subscriptions, status.nextSubscriptionOffset); + } + setGlobal(global); }); @@ -1089,3 +1085,54 @@ addActionHandler('loadStarsTransactions', async (global, actions, payload): Prom } setGlobal(global); }); + +addActionHandler('loadStarsSubscriptions', async (global): Promise => { + const subscriptions = global.stars?.subscriptions; + const offset = subscriptions?.nextOffset; + if (subscriptions && !offset) return; // Already loaded all + + const result = await callApi('fetchStarsSubscriptions', { + offset: offset || '', + }); + + if (!result) { + return; + } + + global = getGlobal(); + + global = updateStarsBalance(global, result.balance); + global = appendStarsSubscriptions(global, result.subscriptions, result.nextOffset); + setGlobal(global); +}); + +addActionHandler('changeStarsSubscription', async (global, actions, payload): Promise => { + const { peerId, id, isCancelled } = payload; + + const peer = peerId ? selectPeer(global, peerId) : undefined; + + if (peerId && !peer) return; + + await callApi('changeStarsSubscription', { + peer, + subscriptionId: id, + isCancelled, + }); + + actions.loadStarStatus(); +}); + +addActionHandler('fulfillStarsSubscription', async (global, actions, payload): Promise => { + const { peerId, id } = payload; + + const peer = peerId ? selectPeer(global, peerId) : undefined; + + if (peerId && !peer) return; + + await callApi('fulfillStarsSubscription', { + peer, + subscriptionId: id, + }); + + actions.loadStarStatus(); +}); diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index e0dd02cda..a14489b3b 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -125,8 +125,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { const chat = selectChat(global, update.id); if (chat && isChatChannel(chat)) { const chatMessages = selectChatMessages(global, update.id); - const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId); - global = deleteChatMessages(global, chat.id, localMessageIds); + if (chatMessages) { + const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId); + global = deleteChatMessages(global, chat.id, localMessageIds); + } } return global; @@ -434,16 +436,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { return global; } - case 'showInvite': { - const { data } = update; - - Object.values(global.byTabId).forEach(({ id: tabId }) => { - actions.showDialog({ data, tabId }); - }); - - return undefined; - } - case 'updatePendingJoinRequests': { const { chatId, requestsPending, recentRequesterIds } = update; const chat = global.chats.byId[chatId]; diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index 430c57e28..b70cf7419 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -176,7 +176,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { } case 'updatePaidReactionPrivacy': { - return { + global = { ...global, settings: { ...global.settings, diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index 5121ae3b1..f42fe4c1e 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -1,5 +1,6 @@ import type { ActionReturnType } from '../../types'; +import { STARS_CURRENCY_CODE } from '../../../config'; import { areDeepEqual } from '../../../util/areDeepEqual'; import { formatCurrencyAsString } from '../../../util/formatCurrency'; import * as langProvider from '../../../util/oldLangProvider'; @@ -19,13 +20,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { if (invoice) { const { amount, currency, title } = invoice; - actions.showNotification({ - tabId, - message: langProvider.oldTranslate('PaymentInfoHint', [ - formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code), - title, - ]), - }); + if (currency !== STARS_CURRENCY_CODE) { + actions.showNotification({ + tabId, + message: langProvider.oldTranslate('PaymentInfoHint', [ + formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code), + title, + ]), + }); + } } if (inputInvoice?.type === 'giftcode') { @@ -42,7 +45,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { isCompleted: true, }, }, tabId); - global = closeInvoice(global, tabId); } } @@ -60,14 +62,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { isCompleted: true, }, }, tabId); - global = closeInvoice(global, tabId); } } if (inputInvoice?.type === 'stars') { - if (!inputInvoice.stars) { - return; - } const starsModalState = selectTabState(global, tabId).starsGiftModal; if (starsModalState && starsModalState.isOpen) { @@ -77,10 +75,27 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { isCompleted: true, }, }, tabId); - global = closeInvoice(global, tabId); } + + actions.loadStarStatus(); // Manually reload. Server update takes ~10 seconds } + if (inputInvoice?.type === 'chatInviteSubscription') { + const { amount } = invoice!; + actions.showNotification({ + tabId, + title: langProvider.oldTranslate('StarsSubscriptionCompleted'), + message: langProvider.oldTranslate('StarsSubscriptionCompletedText', [ + amount, + inputInvoice.inviteInfo.title, + ], undefined, amount), + icon: 'star', + }); + } + + if (invoice?.currency === STARS_CURRENCY_CODE) { + global = closeInvoice(global, tabId); + } setGlobal(global); }); diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index 37490b817..7a8f189d5 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -204,3 +204,10 @@ addActionHandler('requestChatTranslation', (global, actions, payload): ActionRet const { chatId, toLanguageCode, tabId = getCurrentTabId() } = payload; return updateRequestedChatTranslation(global, chatId, toLanguageCode, tabId); }); + +addActionHandler('closeChatInviteModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + return updateTabState(global, { + chatInviteModal: undefined, + }, tabId); +}); diff --git a/src/global/actions/ui/payments.ts b/src/global/actions/ui/payments.ts index 538578405..ba255c69d 100644 --- a/src/global/actions/ui/payments.ts +++ b/src/global/actions/ui/payments.ts @@ -1,12 +1,13 @@ import type { ActionReturnType } from '../../types'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; +import { getStarsTransactionFromGift } from '../../helpers/payments'; import { addActionHandler } from '../../index'; import { clearPayment, closeInvoice, openStarsTransactionModal, updatePayment, } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; -import { selectTabState } from '../../selectors'; +import { selectChatMessage, selectTabState } from '../../selectors'; addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; @@ -97,6 +98,22 @@ addActionHandler('openStarsTransactionModal', (global, actions, payload): Action return openStarsTransactionModal(global, transaction, tabId); }); +addActionHandler('openStarsTransactionFromGift', (global, actions, payload): ActionReturnType => { + const { + chatId, + messageId, + tabId = getCurrentTabId(), + } = payload || {}; + + const message = selectChatMessage(global, chatId, messageId); + if (!message) return undefined; + + const transaction = getStarsTransactionFromGift(message); + if (!transaction) return undefined; + + return openStarsTransactionModal(global, transaction, tabId); +}); + addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; @@ -104,3 +121,21 @@ addActionHandler('closeStarsTransactionModal', (global, actions, payload): Actio starsTransactionModal: undefined, }, tabId); }); + +addActionHandler('openStarsSubscriptionModal', (global, actions, payload): ActionReturnType => { + const { subscription, tabId = getCurrentTabId() } = payload; + + return updateTabState(global, { + starsSubscriptionModal: { + subscription, + }, + }, tabId); +}); + +addActionHandler('closeStarsSubscriptionModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + starsSubscriptionModal: undefined, + }, tabId); +}); diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 2036d8451..e713b26d7 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -4,12 +4,15 @@ import type { ApiChatBannedRights, ApiChatFolder, ApiChatFullInfo, + ApiChatInviteInfo, ApiPeer, ApiTopic, ApiUser, } from '../../api/types'; import type { LangFn } from '../../hooks/useOldLang'; -import type { NotifyException, NotifySettings, ThreadId } from '../../types'; +import type { + CustomPeer, NotifyException, NotifySettings, ThreadId, +} from '../../types'; import { MAIN_THREAD_ID } from '../../api/types'; import { @@ -482,3 +485,16 @@ export function getGroupStatus(lang: LangFn, chat: ApiChat) { ? lang('Subscribers', membersCount, 'i') : lang('Members', membersCount, 'i'); } + +export function getCustomPeerFromInvite(invite: ApiChatInviteInfo): CustomPeer { + const { + title, color, isVerified, isFake, isScam, + } = invite; + return { + isCustomPeer: true, + title, + peerColorId: color, + isVerified, + fakeType: isFake ? 'fake' : isScam ? 'scam' : undefined, + }; +} diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index c9f1fd20b..4ce1164a9 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -53,6 +53,15 @@ export function getRequestInputInvoice( }; } + if (inputInvoice.type === 'chatInviteSubscription') { + const { hash } = inputInvoice; + + return { + type: 'chatInviteSubscription', + hash, + }; + } + if (inputInvoice.type === 'message') { const chat = selectChat(global, inputInvoice.chatId); if (!chat) { diff --git a/src/global/reducers/chats.ts b/src/global/reducers/chats.ts index d2a5e7c43..7473a4c16 100644 --- a/src/global/reducers/chats.ts +++ b/src/global/reducers/chats.ts @@ -385,6 +385,7 @@ export function leaveChat(global: T, leftChatId: string): global = removeChatFromChatLists(global, leftChatId); global = updateChat(global, leftChatId, { isNotJoined: true }); + global = updateChatFullInfo(global, leftChatId, { joinInfo: undefined }); return global; } diff --git a/src/global/reducers/payments.ts b/src/global/reducers/payments.ts index e2d1b265c..d6155a7df 100644 --- a/src/global/reducers/payments.ts +++ b/src/global/reducers/payments.ts @@ -2,6 +2,7 @@ import type { ApiInvoice, ApiPaymentForm, ApiReceiptRegular, ApiReceiptStars, + ApiStarsSubscription, ApiStarsTransaction, } from '../../api/types'; import type { PaymentStep, ShippingOption } from '../../types'; @@ -187,6 +188,29 @@ export function appendStarsTransactions( }; } +export function appendStarsSubscriptions( + global: T, + subscriptions: ApiStarsSubscription[], + nextOffset?: string, +): T { + if (!global.stars) { + return global; + } + + const newObject = { + list: (global.stars.subscriptions?.list || []).concat(subscriptions), + nextOffset, + }; + + return { + ...global, + stars: { + ...global.stars, + subscriptions: newObject, + }, + }; +} + export function openStarsTransactionModal( global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs ): T { diff --git a/src/global/types.ts b/src/global/types.ts index 05c5bd10c..2df9a3952 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -14,6 +14,7 @@ import type { ApiChatBannedRights, ApiChatFolder, ApiChatFullInfo, + ApiChatInviteInfo, ApiChatlistExportedInvite, ApiChatlistInvite, ApiChatReactions, @@ -33,7 +34,6 @@ import type { ApiGroupStatistics, ApiInputInvoice, ApiInputMessageReplyInfo, - ApiInviteInfo, ApiInvoice, ApiKeyboardButton, ApiMediaFormat, @@ -65,6 +65,7 @@ import type { ApiSession, ApiSessionData, ApiSponsoredMessage, ApiStarGiveawayOption, + ApiStarsSubscription, ApiStarsTransaction, ApiStarTopupOption, ApiStealthMode, @@ -183,6 +184,10 @@ export type StarsTransactionHistory = Record; +export type StarsSubscriptions = { + list: ApiStarsSubscription[]; + nextOffset?: string; +}; export type ConfettiStyle = 'poppers' | 'top-down'; @@ -351,6 +356,11 @@ export type TabState = { messageIds: number[]; }; + chatInviteModal?: { + hash: string; + inviteInfo: ApiChatInviteInfo; + }; + seenByModal?: { chatId: string; messageId: number; @@ -595,7 +605,7 @@ export type TabState = { }; notifications: ApiNotification[]; - dialogs: (ApiError | ApiInviteInfo | ApiContact)[]; + dialogs: (ApiError | ApiContact)[]; safeLinkModalUrl?: string; mapModal?: { @@ -748,6 +758,9 @@ export type TabState = { starsTransactionModal?: { transaction: ApiStarsTransaction; }; + starsSubscriptionModal?: { + subscription: ApiStarsSubscription; + }; giftModal?: { isCompleted?: boolean; @@ -1216,6 +1229,7 @@ export type GlobalState = { topupOptions: ApiStarTopupOption[]; balance: number; history: StarsTransactionHistory; + subscriptions?: StarsSubscriptions; }; }; @@ -1337,7 +1351,12 @@ export interface ActionPayloads { adminRights: ApiChatAdminRights; customTitle?: string; } & WithTabId; - acceptInviteConfirmation: { hash: string } & WithTabId; + + checkChatInvite: { + hash: string; + } & WithTabId; + acceptChatInvite: { hash: string } & WithTabId; + closeChatInviteModal: WithTabId | undefined; // settings setSettingOption: Partial | undefined; @@ -1546,9 +1565,6 @@ export interface ActionPayloads { attach?: string; text?: string; } & WithTabId; - openChatByInvite: { - hash: string; - } & WithTabId; toggleSavedDialogPinned: { id: string; } & WithTabId; @@ -1839,6 +1855,10 @@ export interface ActionPayloads { messageId: number; } & WithTabId; closeStarsTransactionModal: WithTabId | undefined; + openStarsSubscriptionModal: { + subscription: ApiStarsSubscription; + } & WithTabId; + closeStarsSubscriptionModal: WithTabId | undefined; openPrizeStarsTransactionFromGiveaway: { chatId: string; messageId: number; @@ -2304,6 +2324,16 @@ export interface ActionPayloads { loadStarsTransactions: { type: StarsTransactionType; }; + loadStarsSubscriptions: undefined; + changeStarsSubscription: { + peerId?: string; + id: string; + isCancelled: boolean; + }; + fulfillStarsSubscription: { + peerId?: string; + id: string; + }; openStarsBalanceModal: { originPayment?: TabState['payment']; originReaction?: { diff --git a/src/lib/gramjs/client/TelegramClient.js b/src/lib/gramjs/client/TelegramClient.js index cd00db33b..e2f9a46bb 100644 --- a/src/lib/gramjs/client/TelegramClient.js +++ b/src/lib/gramjs/client/TelegramClient.js @@ -1000,8 +1000,11 @@ class TelegramClient { if (isExported) this.releaseExportedSender(sender); return result; } catch (e) { - if (e instanceof errors.ServerError || e.message === 'RPC_CALL_FAIL' - || e.message === 'RPC_MCGET_FAIL') { + if (e instanceof errors.ServerError + || e.message === 'RPC_CALL_FAIL' + || e.message === 'RPC_MCGET_FAIL' + || e.message.match(/INTERDC_\d_CALL(_RICH)?_ERROR/) + ) { this._log.warn(`Telegram is having internal issues ${e.constructor.name}`); await sleep(2000); } else if (e instanceof errors.FloodWaitError || e instanceof errors.FloodTestPhoneWaitError) { diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index a390b151e..c9eab76fe 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1654,6 +1654,9 @@ payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payme payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector = payments.StarsStatus; payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector; +payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus; +payments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool; +payments.fulfillStarsSubscription#cc5bebb3 peer:InputPeer subscription_id:string = Bool; payments.getStarsGiveawayOptions#bd1efd3e = Vector; 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; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index d2d4dd8dd..f59ef8b62 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -368,6 +368,9 @@ "payments.getStarsStatus", "payments.getStarsTransactions", "payments.getStarsTransactionsByID", + "payments.getStarsSubscriptions", + "payments.changeStarsSubscription", + "payments.fulfillStarsSubscription", "payments.sendStarsForm", "payments.getStarsGiftOptions", "payments.getStarsGiveawayOptions", diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index b226562c3..924fb726e 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -33,8 +33,12 @@ mask-image: linear-gradient(to right, transparent, black $borderStart, black $borderEnd, transparent); } -@mixin gradient-border-left($indent) { - mask-image: linear-gradient(to right, transparent, black $indent); +@mixin gradient-border-left($indent, $cutout: 0px) { + mask-image: linear-gradient(to right, transparent $cutout, black $indent); +} + +@mixin gradient-border-right($indent, $cutout: 0px) { + mask-image: linear-gradient(to left, transparent $cutout, black $indent); } @mixin gradient-border-top-bottom($top, $bottom) { diff --git a/src/types/index.ts b/src/types/index.ts index 93e564c46..eb5319e48 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,6 +9,7 @@ import type { ApiChatInviteImporter, ApiDocument, ApiExportedInvite, + ApiFakeType, ApiLanguage, ApiMessage, ApiPhoto, @@ -517,19 +518,25 @@ export type InlineBotSettings = { export type CustomPeerType = 'premium' | 'toBeDistributed' | 'contacts' | 'nonContacts' | 'groups' | 'channels' | 'bots' | 'excludeMuted' | 'excludeArchived' | 'excludeRead' | 'stars'; -export interface CustomPeer { +export type CustomPeer = { isCustomPeer: true; key?: string | number; - titleKey: string; subtitleKey?: string; - avatarIcon: IconName; + avatarIcon?: IconName; isAvatarSquare?: boolean; - titleValue?: number; peerColorId?: number; + isVerified?: boolean; + fakeType?: ApiFakeType; customPeerAvatarColor?: string; withPremiumGradient?: boolean; -} +} & ({ + titleKey: string; + title?: undefined; +} | { + title: string; + titleKey?: undefined; +}); -export interface UniqueCustomPeer extends CustomPeer { - type: CustomPeerType; -} +export type UniqueCustomPeer = CustomPeer & { + type: T; +}; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 21b42b2a5..d6e4f156a 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1533,9 +1533,11 @@ export interface LangPair { 'user': string | number; 'link': string | number; }; - 'CreditsBoxOutAbout': { + 'StarsTransactionTOS': { 'link': string | number; }; + 'StarsTransactionTOSLinkText': undefined; + 'StarsTransactionTOSLink': undefined; 'GiftStarsOutgoing': { 'user': string | number; }; @@ -1549,10 +1551,23 @@ export interface LangPair { 'StarsReactionLink': undefined; 'MiniAppsMoreTabs': { 'botName': string | number; + 'count': string | number; }; 'PrizeCredits': { 'count': string | number; }; + 'StarsSubscribeText': { + 'chat': string | number; + 'amount': string | number; + }; + 'StarsSubscribeInfo': { + 'link': string | number; + }; + 'StarsSubscribeInfoLinkText': undefined; + 'StarsSubscribeInfoLink': undefined; + 'StarsPerMonth': { + 'amount': string | number; + }; } diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index be5ee80da..ee1ade814 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -59,7 +59,7 @@ export const processDeepLink = (url: string): boolean => { const params = Object.fromEntries(searchParams); const { - openChatByInvite, + checkChatInvite, openChatByUsername, openChatByPhoneNumber, openStickerSet, @@ -139,7 +139,7 @@ export const processDeepLink = (url: string): boolean => { case 'join': { const { invite } = params; - openChatByInvite({ hash: invite }); + checkChatInvite({ hash: invite }); break; } case 'addemoji': diff --git a/src/util/moduleLoader.ts b/src/util/moduleLoader.ts index bb4f9dae9..4e3332057 100644 --- a/src/util/moduleLoader.ts +++ b/src/util/moduleLoader.ts @@ -6,6 +6,7 @@ export enum Bundles { Main, Extra, Calls, + Stars, } interface ImportedBundles { @@ -13,6 +14,7 @@ interface ImportedBundles { [Bundles.Main]: typeof import('../bundles/main'); [Bundles.Extra]: typeof import('../bundles/extra'); [Bundles.Calls]: typeof import('../bundles/calls'); + [Bundles.Stars]: typeof import('../bundles/stars'); } type BundlePromises = { @@ -46,6 +48,9 @@ export async function loadBundle(bundleName: B) { case Bundles.Calls: LOAD_PROMISES[Bundles.Calls] = import(/* webpackChunkName: "BundleCalls" */ '../bundles/calls'); break; + case Bundles.Stars: + LOAD_PROMISES[Bundles.Stars] = import(/* webpackChunkName: "BundleStars" */ '../bundles/stars'); + break; } (LOAD_PROMISES[bundleName] as Promise).then(runCallbacks); diff --git a/src/util/objects/customPeer.ts b/src/util/objects/customPeer.ts index 287c6677c..ec6e124df 100644 --- a/src/util/objects/customPeer.ts +++ b/src/util/objects/customPeer.ts @@ -10,22 +10,6 @@ export const CUSTOM_PEER_PREMIUM: UniqueCustomPeer = { withPremiumGradient: true, }; -export const CUSTOM_PEER_TO_BE_DISTRIBUTED: UniqueCustomPeer = { - isCustomPeer: true, - type: 'toBeDistributed', - titleKey: 'BoostingToBeDistributed', - avatarIcon: 'user', - withPremiumGradient: true, -}; - -export const CUSTOM_PEER_STAR: UniqueCustomPeer = { - isCustomPeer: true, - type: 'stars', - titleKey: 'Stars', - avatarIcon: 'star', - peerColorId: 1, -}; - export const CUSTOM_PEER_INCLUDED_CHAT_TYPES: UniqueCustomPeer[] = [ { isCustomPeer: true,