From cf539cb173f2d903dcfe2d37ade88ace303cbcb7 Mon Sep 17 00:00:00 2001 From: Denis Shilovich Date: Fri, 23 Aug 2024 08:47:34 +0000 Subject: [PATCH] 1.7.6 (299) --- MODULE.bazel.lock | 28 +- Nicegram/NGStats/Sources/ChatsSharing.swift | 2 +- Package.resolved | 21 +- .../Telegram-iOS/en.lproj/Localizable.strings | 142 +++ .../Sources/AccountContext.swift | 11 +- .../Sources/ChatController.swift | 10 +- .../Sources/GalleryController.swift | 16 +- .../Sources/OpenChatMessage.swift | 4 +- .../AccountContext/Sources/Premium.swift | 3 +- submodules/AdUI/Sources/AdInfoScreen.swift | 13 +- .../AvatarNode/Sources/AvatarNode.swift | 20 +- .../Sources/BotCheckoutControllerNode.swift | 2 +- .../Sources/BrowserBookmarksScreen.swift | 1 + .../Sources/BrowserDocumentContent.swift | 94 +- .../BrowserUI/Sources/BrowserPdfContent.swift | 453 ++++----- .../BrowserUI/Sources/BrowserScreen.swift | 175 +++- .../Sources/BrowserSearchBarComponent.swift | 56 +- .../Sources/BrowserToolbarComponent.swift | 241 +++-- .../BrowserUI/Sources/BrowserWebContent.swift | 26 +- submodules/ChatListUI/BUILD | 2 +- .../Sources/ChatListController.swift | 21 +- .../Sources/ChatListControllerNode.swift | 21 +- .../Sources/ChatListSearchListPaneNode.swift | 19 +- .../Sources/ChatListShimmerNode.swift | 3 +- .../Sources/NicegramButtonComponent.swift | 2 +- .../Sources/Node/ChatListItem.swift | 83 +- .../Sources/Node/ChatListNode.swift | 39 +- .../Sources/Node/ChatListNodeEntries.swift | 1 + .../Sources/Node/ChatListNoticeItem.swift | 41 +- .../Source/Base/Transition.swift | 12 +- .../ReactionButtonListComponent/BUILD | 1 + .../Sources/ReactionButtonListComponent.swift | 211 +++- .../ReactionListContextMenuContent.swift | 23 + .../Sources/ContactsPeerItem.swift | 2 +- .../ContextUI/Sources/ContextController.swift | 14 +- ...tControllerExtractedPresentationNode.swift | 9 +- .../ContextControllerPresentationNode.swift | 2 +- .../Sources/ContextSourceContainer.swift | 8 +- .../Sources/ReactionPreviewView.swift | 2 +- .../DeleteChatPeerActionSheetItem.swift | 2 +- .../ContainedViewLayoutTransition.swift | 11 + .../Navigation/NavigationController.swift | 6 +- .../Source/Navigation/NavigationLayout.swift | 5 + .../Display/Source/PresentationContext.swift | 12 +- submodules/Display/Source/StatusBar.swift | 2 +- .../Display/Source/ViewController.swift | 1 + .../Sources/DrawingLinkEntityView.swift | 2 +- .../Sources/DrawingReactionView.swift | 18 + .../GalleryData/Sources/GalleryData.swift | 8 +- submodules/GalleryUI/BUILD | 4 + .../ChatItemGalleryFooterContentNode.swift | 129 ++- .../GalleryUI/Sources/GalleryController.swift | 5 + .../Sources/Items/ChatImageGalleryItem.swift | 142 ++- .../Items/UniversalVideoGalleryItem.swift | 155 ++- .../Sources/MediaDustNode.swift | 5 + submodules/InviteLinksUI/BUILD | 1 + .../Sources/InviteLinkEditController.swift | 323 ++++-- .../Sources/InviteLinkListController.swift | 20 +- .../Sources/InviteLinkViewController.swift | 192 +++- .../Sources/InviteRequestsController.swift | 2 +- .../Sources/ItemListInviteLinkItem.swift | 98 +- .../ItemListPermanentInviteLinkItem.swift | 57 +- .../Sources/ItemListPeerItem.swift | 100 +- .../Items/ItemListDisclosureItem.swift | 49 +- .../Items/ItemListSingleLineInputItem.swift | 58 +- .../Sources/ResetPasswordController.swift | 6 +- .../Sources/ChannelAdminController.swift | 22 +- .../Sources/ChannelAdminsController.swift | 77 +- .../ChannelBannedMemberController.swift | 24 +- .../Sources/ChannelBlacklistController.swift | 4 +- .../ChannelMembersSearchContainerNode.swift | 24 +- .../ChannelMembersSearchControllerNode.swift | 4 +- .../ChannelPermissionsController.swift | 4 +- .../PeerAllowedReactionListController.swift | 2 +- .../Sources/PlatformRestrictionMatching.swift | 3 + submodules/ReactionSelectionNode/BUILD | 1 + .../Sources/ReactionContextNode.swift | 314 +++++- .../Sources/ReactionSelectionNode.swift | 56 +- .../Source/Signal_Combine.swift | 6 + .../ChangePhoneNumberCodeController.swift | 6 +- .../DataAndStorageSettingsController.swift | 132 ++- .../ProxyServerSettingsController.swift | 8 +- .../CreatePasswordController.swift | 6 +- .../TwoStepVerificationUnlockController.swift | 6 +- .../TextSizeSelectionController.swift | 5 +- .../Themes/ThemePreviewControllerNode.swift | 5 +- .../Sources/ChannelStatsController.swift | 2 +- .../Sources/GroupStatsController.swift | 4 +- .../Sources/StarsTransactionItem.swift | 18 +- submodules/TelegramApi/Sources/Api0.swift | 32 +- submodules/TelegramApi/Sources/Api10.swift | 130 +-- submodules/TelegramApi/Sources/Api11.swift | 110 ++ submodules/TelegramApi/Sources/Api16.swift | 68 +- submodules/TelegramApi/Sources/Api2.swift | 18 +- submodules/TelegramApi/Sources/Api20.swift | 148 +-- submodules/TelegramApi/Sources/Api21.swift | 532 +++------- submodules/TelegramApi/Sources/Api22.swift | 396 ++++++++ submodules/TelegramApi/Sources/Api23.swift | 160 ++- submodules/TelegramApi/Sources/Api3.swift | 22 + submodules/TelegramApi/Sources/Api33.swift | 66 +- submodules/TelegramApi/Sources/Api36.swift | 116 ++- submodules/TelegramApi/Sources/Api4.swift | 90 +- submodules/TelegramApi/Sources/Api6.swift | 30 +- .../Sources/TelegramBaseController.swift | 2 +- .../Sources/VoiceChatController.swift | 10 +- .../Account/AccountIntermediateState.swift | 2 +- .../Sources/Account/AccountManager.swift | 2 + .../Sources/ApiUtils/AdMessageAttribute.swift | 4 +- .../Sources/ApiUtils/ApiGroupOrChannel.swift | 15 +- .../Sources/ApiUtils/BotInfo.swift | 4 +- .../ApiUtils/CachedChannelParticipants.swift | 38 +- .../Sources/ApiUtils/ExportedInvitation.swift | 22 +- .../ApiUtils/ReactionsMessageAttribute.swift | 100 +- .../PendingMessages/EnqueueMessage.swift | 12 +- .../Sources/Settings/ReactionSettings.swift | 2 + .../State/AccountStateManagementUtils.swift | 8 +- .../Sources/State/AccountStateManager.swift | 19 + .../Sources/State/AccountTaskManager.swift | 1 + .../Sources/State/AvailableReactions.swift | 87 +- ...anagedConsumePersonalMessagesActions.swift | 2 + .../Sources/State/ManagedRecentStickers.swift | 18 + .../Sources/State/MessageReactions.swift | 314 +++++- .../Sources/State/Serialization.swift | 2 +- .../Sources/State/UpdatesApiUtils.swift | 2 +- .../TelegramCore/Sources/Suggestions.swift | 1 + .../Sources/SyncCore/SyncCore_BotInfo.swift | 12 +- .../SyncCore/SyncCore_CachedChannelData.swift | 4 +- .../SyncCore/SyncCore_CachedGroupData.swift | 11 +- .../SyncCore_ExportedInvitation.swift | 16 +- .../SyncCore/SyncCore_Namespaces.swift | 1 + .../SyncCore_PeerAccessRestrictionInfo.swift | 6 + .../SyncCore_ReactionsMessageAttribute.swift | 135 ++- .../SyncCore/SyncCore_RecentMediaItem.swift | 22 +- .../SyncCore/SyncCore_TelegramChannel.swift | 43 +- ...yncCore_UpdateMessageReactionsAction.swift | 27 + .../TelegramEngine/Calls/GroupCalls.swift | 2 +- .../TelegramEngine/Data/PeersData.swift | 40 +- .../TelegramEngine/Messages/AdMessages.swift | 37 +- .../Messages/EngineStoryViewListContext.swift | 4 + .../TelegramEngine/Messages/SendAsPeers.swift | 10 +- .../Messages/TelegramEngineMessages.swift | 18 +- .../Payments/BotPaymentForm.swift | 20 +- .../TelegramEngine/Payments/Stars.swift | 520 +++++++++- .../Payments/TelegramEnginePayments.swift | 16 +- .../TelegramEngine/Peers/AddPeerMember.swift | 10 +- .../TelegramEngine/Peers/AddressNames.swift | 4 +- .../Peers/ChannelAdminEventLogs.swift | 3 + .../Peers/ChannelBlacklist.swift | 10 +- .../Peers/ChannelOwnershipTransfer.swift | 4 +- .../Peers/ChannelRecommendation.swift | 2 +- .../TelegramEngine/Peers/Communities.swift | 6 +- .../Peers/InactiveChannels.swift | 2 +- .../Peers/InvitationLinks.swift | 21 +- .../TelegramEngine/Peers/JoinChannel.swift | 66 +- .../TelegramEngine/Peers/JoinLink.swift | 9 +- .../Sources/TelegramEngine/Peers/Peer.swift | 4 + .../TelegramEngine/Peers/PeerAdmins.swift | 10 +- .../TelegramEngine/Peers/SearchPeers.swift | 2 +- .../Peers/TelegramEnginePeers.swift | 8 +- .../Peers/ToggleChannelSignatures.swift | 11 +- .../TelegramEngine/Peers/UpdateBotInfo.swift | 2 +- .../Peers/UpdateCachedPeerData.swift | 23 +- .../UpdatedAccountPrivacySettings.swift | 4 +- .../TelegramCore/Sources/UpdatePeers.swift | 2 +- .../Sources/Utils/MessageUtils.swift | 22 +- .../Sources/Utils/PeerUtils.swift | 31 + .../DefaultDarkPresentationTheme.swift | 12 +- .../DefaultDarkTintedPresentationTheme.swift | 12 +- .../Sources/DefaultDayPresentationTheme.swift | 24 +- .../Sources/ServiceMessageStrings.swift | 11 +- .../Sources/WeatherFormat.swift | 7 +- submodules/TelegramUI/BUILD | 2 +- .../Sources/AdminUserActionsSheet.swift | 4 +- .../AdsInfoScreen/Sources/AdsInfoScreen.swift | 5 +- .../Sources/AdsReportScreen.swift | 3 +- .../Sources/AnimatedTextComponent.swift | 44 +- .../Sources/ChatAvatarNavigationNode.swift | 50 + ...ChatInlineSearchResultsListComponent.swift | 2 + .../ChatMessageAnimatedStickerItemNode.swift | 4 +- .../ChatMessageAttachedContentNode.swift | 6 + .../Sources/ChatMessageBubbleItemNode.swift | 47 +- .../ChatMessageDateAndStatusNode.swift | 2 +- .../StringForMessageTimestampStatus.swift | 16 +- .../ChatMessageInstantVideoItemNode.swift | 4 +- .../ChatMessageInteractiveMediaNode.swift | 50 +- .../Sources/ChatMessageItemImpl.swift | 11 +- ...hatMessageReactionsFooterContentNode.swift | 11 +- .../ChatMessageSelectionInputPanelNode.swift | 2 + .../Sources/ChatMessageStickerItemNode.swift | 4 +- .../ChatMessageTextBubbleContentNode.swift | 2 +- .../ChatMessageWebpageBubbleContentNode.swift | 8 +- .../Sources/ChatRecentActionsController.swift | 5 +- .../ChatRecentActionsControllerNode.swift | 16 +- .../ChatRecentActionsFilterController.swift | 2 +- .../ChatRecentActionsHistoryTransition.swift | 55 +- .../ChatSendAudioMessageContextPreview.swift | 1 + .../Components/Chat/ChatSendStarsScreen/BUILD | 2 + .../Sources/ChatSendStarsScreen.swift | 953 ++++++++++++++++-- .../Sources/ChatShareMessageTagView.swift | 7 + .../Sources/TopMessageReactions.swift | 131 ++- .../Sources/ChatControllerInteraction.swift | 3 + .../Sources/EmojiTextAttachmentView.swift | 2 +- .../Sources/EmojiKeyboardItemLayer.swift | 35 +- .../Sources/EmojiPagerContentComponent.swift | 2 +- .../Sources/EmojiPagerContentSignals.swift | 26 + .../Sources/EmojiSearchHeaderView.swift | 17 +- .../EmojiSearchSearchBarComponent.swift | 21 +- .../Sources/MediaEditorScreen.swift | 2 + .../NotificationExceptionsScreen.swift | 2 +- .../Sources/PeerAllowedReactionsScreen.swift | 91 +- .../PeerInfoScreenPersonalChannelItem.swift | 5 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 2 +- .../Sources/PeerInfoMembers.swift | 4 +- .../Sources/PeerInfoScreen.swift | 38 +- .../Sources/PremiumStarComponent.swift | 205 ++++ ...aticBusinessMessageListItemComponent.swift | 2 + .../Sources/QuickReplySetupScreen.swift | 2 + .../Sources/ItemListReactionItem.swift | 9 + .../QuickReactionSetupController.swift | 7 + .../Sources/ReactionChatPreviewItem.swift | 27 +- .../ThemeAccentColorControllerNode.swift | 3 +- .../Sources/ShareWithPeersScreenState.swift | 4 +- .../TelegramUI/Components/SpaceWarpView/BUILD | 1 + .../SpaceWarpView/Sources/MeshLayer.swift | 7 + .../SpaceWarpView/Sources/SpaceWarpView.swift | 271 ++++- .../Sources/StarsAvatarComponent.swift | 50 +- .../Sources/StarsImageComponent.swift | 45 + .../Sources/StarsPurchaseScreen.swift | 35 +- .../Sources/StarsTransactionScreen.swift | 430 ++++++-- .../Sources/StarsBalanceComponent.swift | 2 +- .../StarsTransactionsListPanelComponent.swift | 14 +- .../Sources/StarsTransactionsScreen.swift | 215 +++- .../Sources/StarsTransferScreen.swift | 163 ++- .../Sources/StarsWithdrawalScreen.swift | 12 +- .../Sources/StorageUsageScreen.swift | 4 +- .../Sources/PeerListItemComponent.swift | 3 + .../Sources/StoryChatContent.swift | 4 + .../Sources/StoryContainerScreen.swift | 4 +- .../Sources/StoryItemOverlaysView.swift | 11 + .../StoryItemSetContainerComponent.swift | 24 +- ...StoryItemSetContainerViewSendMessage.swift | 9 + .../StoryItemSetViewListComponent.swift | 9 + .../InviteLink.imageset/Contents.json | 12 + .../InviteLink.imageset/linklink_40.pdf | Bin 0 -> 1437 bytes .../SubscriptionLink.imageset/Contents.json | 12 + .../SubscriptionLink.imageset/cashlink_40.pdf | Bin 0 -> 2705 bytes .../Stars/BalanceStar.imageset/Contents.json | 2 +- .../BalanceStar.imageset/StarBalance.pdf | Bin 0 -> 7105 bytes .../balancestar_48 (2).pdf | Bin 14697 -> 0 bytes .../Stars/StarLarge.imageset/Contents.json | 2 +- .../Stars/StarLarge.imageset/Star20 (3).pdf | Bin 19311 -> 0 bytes .../Stars/StarLarge.imageset/StarLarge.pdf | Bin 0 -> 7015 bytes .../Stars/StarMedium.imageset/Contents.json | 2 +- .../Stars/StarMedium.imageset/StarMedium.pdf | Bin 0 -> 6974 bytes .../Stars/StarMedium.imageset/star_18 (3).pdf | Bin 19197 -> 0 bytes .../StarMediumOutline.imageset/Contents.json | 12 + .../StarOutline.pdf | Bin 0 -> 16644 bytes .../Stars/StarSmall.imageset/Contents.json | 2 +- .../Stars/StarSmall.imageset/StarSmall.pdf | Bin 0 -> 6946 bytes .../Stars/StarSmall.imageset/star_16 (3).pdf | Bin 19140 -> 0 bytes .../TransactionStar.imageset/Contents.json | 12 + .../StarTransaction.pdf | Bin 0 -> 7031 bytes .../Contents.json | 12 + .../StarTransactionOutline.pdf | Bin 0 -> 17106 bytes .../star_up/star_reaction_activate.tgs | Bin 0 -> 5659 bytes .../star_up/star_reaction_appear.tgs | Bin 0 -> 5796 bytes .../star_up/star_reaction_center.tgs | Bin 0 -> 5659 bytes .../star_up/star_reaction_effect.tgs | Bin 0 -> 8420 bytes .../star_up/star_reaction_select.tgs | Bin 0 -> 5659 bytes .../star_up/star_reaction_static_icon.webp | Bin 0 -> 2850 bytes .../TelegramUI/Sources/AccountContext.swift | 2 + .../Chat/ChatControllerLoadDisplayNode.swift | 24 +- ...ChatControllerOpenMessageContextMenu.swift | 224 ++-- ...atControllerOpenViewOnceMediaMessage.swift | 2 +- .../Chat/ChatControllerOpenWebApp.swift | 72 +- .../Chat/PeerMessageSelectedReactions.swift | 5 +- .../ChatAgeRestrictionAlertController.swift | 9 +- .../TelegramUI/Sources/ChatController.swift | 443 +++++--- .../Sources/ChatControllerAdminBanUsers.swift | 8 +- .../Sources/ChatControllerNode.swift | 8 + ...rollerOpenMessageReactionContextMenu.swift | 222 +++- .../Sources/ChatControllerRemoveAd.swift | 2 +- .../Sources/ChatHistoryEntriesForView.swift | 57 +- .../Sources/ChatHistoryListNode.swift | 16 +- .../ChatInterfaceStateContextMenus.swift | 24 +- .../ChatSearchResultsContollerNode.swift | 1 + .../ChatSearchTitleAccessoryPanelNode.swift | 6 +- .../CommandChatInputContextPanelNode.swift | 2 + .../Sources/ManagedAudioRecorder.swift | 2 + .../Sources/Nicegram/NGDeeplinkHandler.swift | 6 +- .../TelegramUI/Sources/OpenChatMessage.swift | 34 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 88 +- submodules/TelegramUI/Sources/OpenUrl.swift | 9 +- .../OverlayAudioPlayerControllerNode.swift | 3 +- .../Sources/PollResultsController.swift | 2 +- .../Sources/SharedAccountContext.swift | 31 +- .../Sources/TelegramRootController.swift | 14 +- .../TelegramUI/Sources/TextLinkHandling.swift | 12 +- .../ChannelMemberCategoryListContext.swift | 10 +- ...annelMemberCategoriesContextsManager.swift | 2 +- .../Source/UIKitRuntimeUtils/UIKitUtils.h | 2 + .../Source/UIKitRuntimeUtils/UIKitUtils.m | 14 + submodules/UndoUI/BUILD | 3 + .../Sources/UndoOverlayController.swift | 5 +- .../Sources/UndoOverlayControllerNode.swift | 125 ++- .../UrlHandling/Sources/UrlHandling.swift | 4 +- .../WebUI/Sources/WebAppController.swift | 50 +- .../WebAppLaunchConfirmationController.swift | 33 +- versions.json | 2 +- 309 files changed, 9686 insertions(+), 3062 deletions(-) create mode 100644 submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/linklink_40.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Item List/SubscriptionLink.imageset/cashlink_40.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/BalanceStar.imageset/StarBalance.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/BalanceStar.imageset/balancestar_48 (2).pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Star20 (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/StarLarge.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/StarMedium.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/star_18 (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/StarOutline.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/StarSmall.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/star_16 (3).pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/StarTransaction.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/StarTransactionOutline.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_activate.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_appear.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_center.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_select.tgs create mode 100644 submodules/TelegramUI/Resources/Animations/star_up/star_reaction_static_icon.webp diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index bab431a75d6..1a65d263899 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -2956,8 +2956,8 @@ "general": { "bzlTransitiveDigest": "YjE3dFjYQ4sj5gn2Iz1cWVK14/ZJ5cmnAUUGjDbACAA=", "recordedFileInputs": { - "@@//Package.resolved": "06bfbab719e4a9a5c518560ccd38f94fac37034aa6d6b4ca345c59df1237d28a", - "@@//Package.swift": "5ec7a4bc98efbe05c2f49eace2c89b1ef5b01d545d1474b5a2e2a2ad98c32a2e" + "@@//Package.resolved": "d81120287c19acb5e544c11ee5393145dcc4c12c81b62bcc19d5576dab06a370", + "@@//Package.swift": "300a72a2cbc0256b09d99520341bf9a81b4d296bdd151b64665889820664a83d" }, "recordedDirentsInputs": {}, "envVariables": {}, @@ -3052,6 +3052,24 @@ "patches": [] } }, + "swiftpkg_swiftui_flow": { + "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", + "ruleClassName": "swift_package", + "attributes": { + "bazel_package_name": "swiftpkg_swiftui_flow", + "commit": "9d122ace53e143dc3e1bf61c01a024535b0c7ab7", + "remote": "https://github.com/denis15yo/SwiftUI-Flow.git", + "init_submodules": false, + "recursive_init_submodules": true, + "patch_args": [ + "-p0" + ], + "patch_cmds": [], + "patch_cmds_win": [], + "patch_tool": "", + "patches": [] + } + }, "swiftpkg_swiftystorekit": { "bzlFile": "@@rules_swift_package_manager~//swiftpkg/internal:swift_package.bzl", "ruleClassName": "swift_package", @@ -3165,7 +3183,7 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_core_swift", - "commit": "f03e7c89c56aaafdb3b22a8ab5ebfefb3fb018a6", + "commit": "8fe212b4616cce8f534cc4708c8d6b2b548a5035", "remote": "https://github.com/denis15yo/core-swift.git", "init_submodules": false, "recursive_init_submodules": true, @@ -3417,7 +3435,7 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_nicegram_assistant_ios", - "commit": "1b82457f99a129e8c97d8314b405e1e0e411a8a6", + "commit": "741126d3991a3973a79cc7ecb81d84ece4649159", "remote": "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "init_submodules": false, "recursive_init_submodules": true, @@ -3669,7 +3687,7 @@ "ruleClassName": "swift_package", "attributes": { "bazel_package_name": "swiftpkg_nicegram_wallet_ios", - "commit": "794177222256849249416a30fa8e3dc7560507c8", + "commit": "d726273283da0db83939374137006f808bfd4d4c", "remote": "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "init_submodules": false, "recursive_init_submodules": true, diff --git a/Nicegram/NGStats/Sources/ChatsSharing.swift b/Nicegram/NGStats/Sources/ChatsSharing.swift index 74898560026..095df05240b 100644 --- a/Nicegram/NGStats/Sources/ChatsSharing.swift +++ b/Nicegram/NGStats/Sources/ChatsSharing.swift @@ -269,7 +269,7 @@ private func extractGeoLocation( private func extractInviteLinks(_ links: ExportedInvitations?) -> [InviteLink]? { links?.list?.compactMap { link in switch link { - case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): + case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, _): InviteLink(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId.id._internalGetInt64Value(), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount) case .publicJoinRequest: nil diff --git a/Package.resolved b/Package.resolved index cba5a86dbd9..6dfa6e8e618 100644 --- a/Package.resolved +++ b/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/denis15yo/core-swift.git", "state" : { "branch" : "release/1.0.0", - "revision" : "ff29d19dc288c4b46f1ba16df1a9b320af384e9b" + "revision" : "78f8920a260775686dd0e04f5045677447bb7a6c" } }, { @@ -123,7 +123,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-assistant-ios.git", "state" : { "branch" : "master", - "revision" : "e9129c74ffc2bb0ff74b9ee887bcf37c5a8a00c9" + "revision" : "ae5eb736957ce58b2d3e876df90ecbd252d13fe4" } }, { @@ -132,7 +132,7 @@ "location" : "git@bitbucket.org:mobyrix/nicegram-wallet-ios.git", "state" : { "branch" : "master", - "revision" : "64428c3992ef989f6e9fc943d111c21cc895f4d5" + "revision" : "435678b5aa662b6482f1b56ba904d64526e4b9c6" } }, { @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "5191b801aca999b704eb93f118f91468b4570571", - "version" : "5.19.6" + "revision" : "8a1be70a625683bc04d6903e2935bf23f3c6d609", + "version" : "5.19.7" } }, { @@ -288,6 +288,15 @@ "version" : "1.1.6" } }, + { + "identity" : "swiftui-flow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/denis15yo/SwiftUI-Flow.git", + "state" : { + "branch" : "main", + "revision" : "9d122ace53e143dc3e1bf61c01a024535b0c7ab7" + } + }, { "identity" : "swiftystorekit", "kind" : "remoteSourceControl", @@ -321,7 +330,7 @@ "location" : "https://github.com/denis15yo/ton-swift.git", "state" : { "branch" : "main", - "revision" : "385a83b922f6c4ba1fdb26128086ae35f95893d4" + "revision" : "e08680928f8fd83319c47f1f48301a52ba502b8b" } }, { diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3291c1b68d5..706a1a80619 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9197,6 +9197,8 @@ Sorry for the inconvenience."; "WebApp.LaunchMoreInfo" = "More about this bot"; "WebApp.LaunchConfirmation" = "To launch this web app, you will connect to its website."; +"WebApp.LaunchTermsConfirmation" = "By launching this mini app, you agree to the [Terms of Service for Mini Apps]()."; +"WebApp.LaunchTermsConfirmation_URL" = "https://telegram.org/tos/mini-apps"; "WebApp.LaunchOpenApp" = "Open App"; "WallpaperPreview.PreviewInNightMode" = "Preview this wallpaper in night mode."; @@ -12344,6 +12346,7 @@ Sorry for the inconvenience."; "Stars.Transfer.Purchased.Stars_any" = "%@ Stars"; "Stars.Transfer.UnlockedText" = "You unlocked media for **%1$@**."; "Stars.Transfer.UnlockInfo" = "Do you want to unlock %1$@ in **%2$@** for **%3$@**?"; +"Stars.Transfer.UnlockBotInfo" = "Do you want to unlock %1$@ from **%2$@** for **%3$@**?"; "Stars.Transfer.Balance" = "Balance"; @@ -12516,6 +12519,7 @@ Sorry for the inconvenience."; "Notification.StarsGift.Title_any" = "%@ Stars"; "Notification.StarsGift.Subtitle" = "Use Stars to unlock content and services on Telegram."; "Notification.StarsGift.SubtitleYou" = "With Stars, %@ will be able to unlock content and services on Telegram."; +"Notification.StarsGift.UnknownUser" = "Unknown user"; "Bot.Settings" = "Bot Settings"; @@ -12544,6 +12548,11 @@ Sorry for the inconvenience."; "WebBrowser.LinkForwardTooltip.ManyChats.One" = "Link forwarded to **%@** and %@ others"; "WebBrowser.LinkForwardTooltip.SavedMessages.One" = "Link forwarded to **Saved Messages**"; +"WebBrowser.FileForwardTooltip.Chat.One" = "Document forwarded to **%@**"; +"WebBrowser.FileForwardTooltip.TwoChats.One" = "Document forwarded to **%@** and **%@**"; +"WebBrowser.FileForwardTooltip.ManyChats.One" = "Document forwarded to **%@** and %@ others"; +"WebBrowser.FileForwardTooltip.SavedMessages.One" = "Document forwarded to **Saved Messages**"; + "Stars.Intro.StarsSent_1" = "%@ Star sent."; "Stars.Intro.StarsSent_any" = "%@ Stars sent."; "Stars.Intro.StarsSent.ViewChat" = "View Chat"; @@ -12672,3 +12681,136 @@ Sorry for the inconvenience."; "Stars.Intro.GiftStars" = "Gift Stars to Friends"; "MediaPicker.CreateSticker" = "Create a sticker from a photo"; + +"Channel.AdminLog.MessageToggleProfileSignaturesOn" = "%@ enabled admin profiles"; +"Channel.AdminLog.MessageToggleProfileSignaturesOff" = "%@ disabled admin profiles"; + +"Stickers.CreateSticker" = "Create\nSticker"; + +"InviteLink.CreateNewInfo" = "You can create additional invite links that are limited by time, number of users, or require a paid subscription."; + +"InviteLink.CopyShort" = "Copy"; +"InviteLink.ShareShort" = "Share"; + +"Stars.Subscription.Terms" = "By subscribing you agree to the [Terms of Service]()."; +"Stars.Subscription.Terms_URL" = "https://telegram.org/tos/stars"; + +"Stars.Transaction.Reaction.Title" = "Star reaction"; +"Stars.Transaction.Reaction.Post" = "Post"; + +"Stars.Transaction.SubscriptionFee" = "Monthly Subscription Fee"; +"Stars.Transaction.Subscription.Title" = "Subscription"; +"Stars.Transaction.Subscription.Active" = "If you cancel now, you can still access your subscription until %@"; +"Stars.Transaction.Subscription.Cancelled" = "You have cancelled your subscription."; +"Stars.Transaction.Subscription.Renew" = "Renew Subscription"; +"Stars.Transaction.Subscription.Cancel" = "Cancel Subscription"; +"Stars.Transaction.Subscription.JoinChannel" = "Join Channel"; +"Stars.Transaction.Subscription.JoinAgainChannel" = "Join Channel"; +"Stars.Transaction.Subscription.LeftChannel" = "You left channel but you can still get back until %@"; +"Stars.Transaction.Subscription.Expired" = "Your subscription expired on %@."; +"Stars.Transaction.Subscription.PerMonth" = "%@ / month"; +"Stars.Transaction.Subscription.PerMonthUsd" = "appx. %@ per month"; +"Stars.Transaction.Subscription.Subscription" = "Subscription"; +"Stars.Transaction.Subscription.Subscriber" = "Subscriber"; + +"Stars.Transaction.Subscription.Status.Expires" = "Expires"; +"Stars.Transaction.Subscription.Status.Expired" = "Expired"; +"Stars.Transaction.Subscription.Status.Renews" = "Renews"; +"Stars.Transaction.Subscription.Status.Subscribed" = "Subscribed"; + +"Stars.Transaction.Subscription.Cancelled.Title" = "Subscription cancelled"; +"Stars.Transaction.Subscription.Cancelled.Text" = "You will still have access to [%1$@]() until %2$@."; + +"Stars.Transaction.Subscription.Renewed.Title" = "Subscription renewed"; +"Stars.Transaction.Subscription.Renewed.Text" = "You renewed your subscription to [%1$@]()."; + +"Stars.Intro.Subscriptions.Title" = "MY SUBSCRIPTIONS"; +"Stars.Intro.Subscriptions.PerMonth" = "per month"; +"Stars.Intro.Subscriptions.Renews" = "renews on %@"; +"Stars.Intro.Subscriptions.Expires" = "expires on %@"; +"Stars.Intro.Subscriptions.Expired" = "expired on %@"; +"Stars.Intro.Subscriptions.Cancelled" = "cancelled"; +"Stars.Intro.Subscriptions.ExpiredStatus" = "expired"; +"Stars.Intro.Subscriptions.ShowMore" = "Show More"; + +"Stars.Intro.Transaction.SubscriptionFee.Title" = "Monthly Subscription Fee"; +"Stars.Intro.Transaction.Reaction.Title" = "Star Reaction"; + +"Stars.Purchase.GenericPurchasePurpose" = "Buy Stars to unlock content and services on Telegram."; +"Stars.Purchase.PurchasePurpose.subs" = "Buy Stars to keep all your subscriptions."; + +"Stars.Transfer.Subscribe.Channel.Title" = "Subscribe"; +"Stars.Transfer.SubscribeInfo" = "Do you want to subscribe to **%1$@** for **%2$@** per month?"; +"Stars.Transfer.Subscribe" = "Subscribe"; +"Stars.Transfer.Subscribe.Successful.Title" = "Subscription successful!"; +"Stars.Transfer.Subscribe.Successful.Text" = "%1$@ transferred to %2$@."; + +"Gallery.Ad" = "Ad"; + +"Chat.SensitiveContent" = "18+ Content"; + +"Settings.SensitiveContent" = "Show 18+ Content"; +"Settings.SensitiveContentInfo" = "Do not hide media that contain content suitable only for adults."; + +"SensitiveContent.Title" = "18+ Content"; +"SensitiveContent.Text" = "This media may contain sensitive content suitable only for adults.\nDo you still want to view it?"; +"SensitiveContent.ShowAlways" = "Always show 18+ media"; +"SensitiveContent.ViewAnyway" = "View Anyway"; +"SensitiveContent.SettingsInfo" = "You can update the visibility of sensitive media in [Data and Storage > Show 18+ Content]()."; + +"SensitiveContent.Enable.Title" = "18+ Content"; +"SensitiveContent.Enable.Text" = "Confirm that you are over 18 years old and update settings to see potentially explicit and sensitive content."; +"SensitiveContent.Enable.Confirm" = "Confirm"; + +"Notification.Refund" = "You received a refund of {amount} from {name}"; + +"InviteLink.SubscriptionFee.Title" = "SUBSCRIPTION FEE"; +"InviteLink.SubscriptionFee.PerMonth" = "%@ / month"; +"InviteLink.SubscriptionFee.NoOneJoined" = "No one joined yet"; +"InviteLink.SubscriptionFee.ApproximateIncome" = "You get approximately %@ monthly"; + +"InviteLink.PerMonth" = "per month"; + +"InviteLink.Create.Fee" = "Require Monthly Fee"; +"InviteLink.Create.FeePerMonth" = "%@ / month"; +"InviteLink.Create.FeePlaceholder" = "Stars amount per month"; +"InviteLink.Create.FeeInfo" = "Charge a subscription fee from people joining your channel via this link. [Learn More >]()"; +"InviteLink.Create.FeeEditInfo" = "If you need to change the subscription fee, create a new invite link with a different price."; +"InviteLink.Create.RequestApprovalFeeUnavailable" = "You can't enable admin approval for links that require a monthly fee."; + +"WebApp.PrivacyPolicy_URL" = "https://telegram.org/privacy-tpa"; + +"ChatList.SubscriptionsLowBalance.Stars_1" = "%@ Star needed"; +"ChatList.SubscriptionsLowBalance.Stars_any" = "%@ Stars needed"; + +"ChatList.SubscriptionsLowBalance.Single.Title" = "%1$@ for %2$@"; +"ChatList.SubscriptionsLowBalance.Single.Text" = "Insufficient funds to cover your subscription."; + +"ChatList.SubscriptionsLowBalance.Multiple.Title" = "%@ for your subscriptions"; +"ChatList.SubscriptionsLowBalance.Multiple.Text" = "Insufficient funds to cover your subscriptions."; + +"ChatList.Search.SectionApps" = "APPS"; + +"Channel.ShowAuthors" = "Show Authors' Profiles"; +"Channel.ShowAuthorsFooter" = "Add names and photos of admins to the messages they post, linking to their profiles."; + +"SendStarReactions.Title" = "React with Stars"; +"SendStarReactions.Balance" = "Balance"; +"SendStarReactions.UserLabelAnonymous" = "Anonymous"; +"SendStarReactions.SliderTop" = "TOP"; +"SendStarReactions.TextSentStars_1" = "You sent **1** star to support this post."; +"SendStarReactions.TextSentStars_any" = "You sent **%d** stars to support this post."; +"SendStarReactions.TextGeneric" = "Choose how many stars you want to send to **%@** to support this post."; +"SendStarReactions.SectionTop" = "Top Senders"; +"SendStarReactions.ShowMyselfInTop" = "Show me in Top Senders"; +"SendStarReactions.SendButtonTitle" = "Send # %@"; +"SendStarReactions.TermsOfServiceFooter" = "By sending Stars you agree to the [Terms of Service](https://telegram.org/tos/stars)"; + +"PeerInfo.AllowedReactions.StarReactions" = "Enable Paid Reactions"; +"PeerInfo.AllowedReactions.StarReactionsFooter" = "Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)"; + +"Chat.ToastStarsSent.Title_1" = "Star sent!"; +"Chat.ToastStarsSent.Title_any" = "Stars sent!"; +"Chat.ToastStarsSent.Text" = "You have reacted with %1$@ %2$@."; +"Chat.ToastStarsSent.TextStarAmount_1" = "star"; +"Chat.ToastStarsSent.TextStarAmount_any" = "stars"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 2567bd2fdad..3c61855e8e1 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -305,7 +305,7 @@ public enum ResolvedUrl { case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?) case invoice(slug: String, invoice: TelegramMediaInvoice?) case premiumOffer(reference: String?) - case starsTopup(amount: Int64?) + case starsTopup(amount: Int64, purpose: String?) case chatFolder(slug: String) case story(peerId: PeerId, id: Int32) case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) @@ -923,7 +923,7 @@ public protocol SharedAccountContext: AnyObject { func makeProxySettingsController(context: AccountContext) -> ViewController func makeLocalizationListController(context: AccountContext) -> ViewController func makeCreateGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String?, mode: CreateGroupMode, completion: ((PeerId, @escaping () -> Void) -> Void)?) -> ViewController - func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController + func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController func makePrivacyAndSecurityController(context: AccountContext) -> ViewController func makeBioPrivacyController(context: AccountContext, settings: Promise, present: @escaping (ViewController) -> Void) func makeBirthdayPrivacyController(context: AccountContext, settings: Promise, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void) @@ -1002,6 +1002,8 @@ public protocol SharedAccountContext: AnyObject { func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController + func makeDataAndStorageController(context: AccountContext, sensitiveContent: Bool) -> ViewController + func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController @@ -1010,9 +1012,12 @@ public protocol SharedAccountContext: AnyObject { func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController - func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController + func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController + func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController + func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController + func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index c2bfca00d4c..315d1ec2541 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -62,6 +62,7 @@ public final class ChatMessageItemAssociatedData: Equatable { public let deviceContactsNumbers: Set public let isStandalone: Bool public let isInline: Bool + public let showSensitiveContent: Bool public init( automaticDownloadPeerType: MediaAutoDownloadPeerType, @@ -94,7 +95,8 @@ public final class ChatMessageItemAssociatedData: Equatable { chatThemes: [TelegramTheme] = [], deviceContactsNumbers: Set = Set(), isStandalone: Bool = false, - isInline: Bool = false + isInline: Bool = false, + showSensitiveContent: Bool = false ) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId @@ -127,6 +129,7 @@ public final class ChatMessageItemAssociatedData: Equatable { self.deviceContactsNumbers = deviceContactsNumbers self.isStandalone = isStandalone self.isInline = isInline + self.showSensitiveContent = showSensitiveContent } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -217,6 +220,9 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.isInline != rhs.isInline { return false } + if lhs.showSensitiveContent != rhs.showSensitiveContent { + return false + } return true } } @@ -1056,6 +1062,8 @@ public protocol ChatController: ViewController { func updateIsScrollingLockedAtTop(isScrollingLockedAtTop: Bool) func playShakeAnimation() + + func removeAd(opaqueId: Data) } public protocol ChatMessagePreviewItemNode: AnyObject { diff --git a/submodules/AccountContext/Sources/GalleryController.swift b/submodules/AccountContext/Sources/GalleryController.swift index 33b8a49c8ff..3342beb0a80 100644 --- a/submodules/AccountContext/Sources/GalleryController.swift +++ b/submodules/AccountContext/Sources/GalleryController.swift @@ -17,18 +17,32 @@ public final class GalleryControllerActionInteraction { public let openPeer: (EnginePeer) -> Void public let openHashtag: (String?, String) -> Void public let openBotCommand: (String) -> Void + public let openAd: (MessageId) -> Void public let addContact: (String) -> Void public let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void public let editMedia: (MessageId, [UIView], @escaping () -> Void) -> Void public let updateCanReadHistory: (Bool) -> Void - public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void, updateCanReadHistory: @escaping (Bool) -> Void) { + public init( + openUrl: @escaping (String, Bool) -> Void, + openUrlIn: @escaping (String) -> Void, + openPeerMention: @escaping (String) -> Void, + openPeer: @escaping (EnginePeer) -> Void, + openHashtag: @escaping (String?, String) -> Void, + openBotCommand: @escaping (String) -> Void, + openAd: @escaping (MessageId) -> Void, + addContact: @escaping (String) -> Void, + storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void, + editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void, + updateCanReadHistory: @escaping (Bool) -> Void) + { self.openUrl = openUrl self.openUrlIn = openUrlIn self.openPeerMention = openPeerMention self.openPeer = openPeer self.openHashtag = openHashtag self.openBotCommand = openBotCommand + self.openAd = openAd self.addContact = addContact self.storeMediaPlaybackState = storeMediaPlaybackState self.editMedia = editMedia diff --git a/submodules/AccountContext/Sources/OpenChatMessage.swift b/submodules/AccountContext/Sources/OpenChatMessage.swift index ade6e84c968..25c58ae4dcf 100644 --- a/submodules/AccountContext/Sources/OpenChatMessage.swift +++ b/submodules/AccountContext/Sources/OpenChatMessage.swift @@ -32,7 +32,7 @@ public final class OpenChatMessageParams { public let navigationController: NavigationController? public let modal: Bool public let dismissInput: () -> Void - public let present: (ViewController, Any?) -> Void + public let present: (ViewController, Any?, PresentationContextType) -> Void public let transitionNode: (MessageId, Media, Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? public let addToTransitionSurface: (UIView) -> Void public let openUrl: (String) -> Void @@ -63,7 +63,7 @@ public final class OpenChatMessageParams { navigationController: NavigationController?, modal: Bool = false, dismissInput: @escaping () -> Void, - present: @escaping (ViewController, Any?) -> Void, + present: @escaping (ViewController, Any?, PresentationContextType) -> Void, transitionNode: @escaping (MessageId, Media, Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, addToTransitionSurface: @escaping (UIView) -> Void, openUrl: @escaping (String) -> Void, diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 183c5aeea7a..b3950170573 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -123,7 +123,8 @@ public enum BoostSubject: Equatable { } public enum StarsPurchasePurpose: Equatable { - case generic(requiredStars: Int64?) + case generic + case topUp(requiredStars: Int64, purpose: String?) case transfer(peerId: EnginePeer.Id, requiredStars: Int64) case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool) case gift(peerId: EnginePeer.Id) diff --git a/submodules/AdUI/Sources/AdInfoScreen.swift b/submodules/AdUI/Sources/AdInfoScreen.swift index 4bdbf3a484c..9a3309f9b84 100644 --- a/submodules/AdUI/Sources/AdInfoScreen.swift +++ b/submodules/AdUI/Sources/AdInfoScreen.swift @@ -69,7 +69,7 @@ public final class AdInfoScreen: ViewController { self.controller = controller self.context = context - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = controller.presentationData self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 1 @@ -211,11 +211,16 @@ public final class AdInfoScreen: ViewController { } private let context: AccountContext - private var presentationData: PresentationData + fileprivate var presentationData: PresentationData - public init(context: AccountContext) { + public init(context: AccountContext, forceDark: Bool = false) { self.context = context - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = presentationData super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 7d7556f6889..ecf20247bef 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -97,8 +97,12 @@ private func calculateColors(context: AccountContext?, explicitColorIndex: Int?, colors = AvatarNode.repostColors } else if case .repliesIcon = icon { colors = AvatarNode.savedMessagesColors - } else if case .anonymousSavedMessagesIcon = icon { - colors = AvatarNode.savedMessagesColors + } else if case let .anonymousSavedMessagesIcon(isColored) = icon { + if isColored { + colors = AvatarNode.savedMessagesColors + } else { + colors = AvatarNode.grayscaleColors + } } else if case .myNotesIcon = icon { colors = AvatarNode.savedMessagesColors } else if case .editAvatarIcon = icon, let theme { @@ -181,7 +185,7 @@ private enum AvatarNodeIcon: Equatable { case none case savedMessagesIcon case repliesIcon - case anonymousSavedMessagesIcon + case anonymousSavedMessagesIcon(isColored: Bool) case myNotesIcon case archivedChatsIcon(hiddenByDefault: Bool) case editAvatarIcon @@ -195,7 +199,7 @@ public enum AvatarNodeImageOverride: Equatable { case image(TelegramMediaImageRepresentation) case savedMessagesIcon case repliesIcon - case anonymousSavedMessagesIcon + case anonymousSavedMessagesIcon(isColored: Bool) case myNotesIcon case archivedChatsIcon(hiddenByDefault: Bool) case editAvatarIcon(forceNone: Bool) @@ -509,9 +513,9 @@ public final class AvatarNode: ASDisplayNode { case .repliesIcon: representation = nil icon = .repliesIcon - case .anonymousSavedMessagesIcon: + case let .anonymousSavedMessagesIcon(isColored): representation = nil - icon = .anonymousSavedMessagesIcon + icon = .anonymousSavedMessagesIcon(isColored: isColored) case .myNotesIcon: representation = nil icon = .myNotesIcon @@ -685,9 +689,9 @@ public final class AvatarNode: ASDisplayNode { case .repliesIcon: representation = nil icon = .repliesIcon - case .anonymousSavedMessagesIcon: + case let .anonymousSavedMessagesIcon(isColored): representation = nil - icon = .anonymousSavedMessagesIcon + icon = .anonymousSavedMessagesIcon(isColored: isColored) case .myNotesIcon: representation = nil icon = .myNotesIcon diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index c66dec6529f..35b4c1f46de 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1524,7 +1524,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz } switch result { - case let .done(receiptMessageId): + case let .done(receiptMessageId, _): proceedWithCompletion(true, receiptMessageId) case let .externalVerificationRequired(url): strongSelf.updateActionButton() diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index b77e831e36f..cef645b788d 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -166,6 +166,7 @@ public final class BrowserBookmarksScreen: ViewController { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 316ab28538a..c17a2911ed8 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -20,6 +20,7 @@ import UrlEscaping final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData + let file: TelegramMediaFile private let webView: WKWebView @@ -50,6 +51,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate self.context = context self.uuid = UUID() self.presentationData = presentationData + self.file = file let configuration = WKWebViewConfiguration() self.webView = WKWebView(frame: CGRect(), configuration: configuration) @@ -60,6 +62,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } var title: String = "file" + var url = "" if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { var updatedPath = path if let fileName = file.fileName { @@ -67,13 +70,14 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate updatedPath = tempFile.path self.tempFile = tempFile title = fileName + url = updatedPath } let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) self.webView.load(request) } - self._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) self.statePromise = Promise(self._state) super.init(frame: .zero) @@ -258,28 +262,6 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right) - -// if let error = self.currentError { -// let errorSize = self.errorView.update( -// transition: .immediate, -// component: AnyComponent( -// ErrorComponent( -// theme: self.presentationData.theme, -// title: self.presentationData.strings.Browser_ErrorTitle, -// text: error.localizedDescription -// ) -// ), -// environment: {}, -// containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height) -// ) -// if self.errorView.superview == nil { -// self.addSubview(self.errorView) -// self.errorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) -// } -// self.errorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - errorSize.width) / 2.0), y: insets.top + floorToScreenPixels((size.height - insets.top - insets.bottom - errorSize.height) / 2.0)), size: errorSize) -// } else if self.errorView.superview != nil { -// self.errorView.removeFromSuperview() -// } } private func updateState(_ f: (BrowserContentState) -> BrowserContentState) { @@ -365,7 +347,6 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { -// self.currentError = nil self.updateFontState(self.currentFontState, force: true) } @@ -376,29 +357,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) } } - -// func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { -// if (error as NSError).code != -999 { -// self.currentError = error -// } else { -// self.currentError = nil -// } -// if let (size, insets) = self.validLayout { -// self.updateLayout(size: size, insets: insets, transition: .immediate) -// } -// } -// -// func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { -// if (error as NSError).code != -999 { -// self.currentError = error -// } else { -// self.currentError = nil -// } -// if let (size, insets) = self.validLayout { -// self.updateLayout(size: size, insets: insets, transition: .immediate) -// } -// } - + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil { if let url = navigationAction.request.url?.absoluteString { @@ -417,37 +376,6 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate decisionHandler(.prompt) } - -// @available(iOS 13.0, *) -// func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { -// guard let url = elementInfo.linkURL else { -// completionHandler(nil) -// return -// } -// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } -// let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in -// return UIMenu(title: "", children: [ -// UIAction(title: presentationData.strings.Browser_ContextMenu_Open, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// self?.open(url: url.absoluteString, new: false) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_OpenInNewTab, image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// self?.open(url: url.absoluteString, new: true) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_AddToReadingList, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in -// let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// UIPasteboard.general.string = url.absoluteString -// self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// self?.share(url: url.absoluteString) -// }) -// ]) -// } -// completionHandler(configuration) -// } - private func open(url: String, new: Bool) { let subject: BrowserScreen.Subject = .webPage(url: url) if new, let navigationController = self.getNavigationController() { @@ -474,6 +402,14 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate } func makeContentSnapshotView() -> UIView? { - return nil + let configuration = WKSnapshotConfiguration() + configuration.rect = CGRect(origin: .zero, size: self.webView.frame.size) + + let imageView = UIImageView() + imageView.frame = CGRect(origin: .zero, size: self.webView.frame.size) + self.webView.takeSnapshot(with: configuration, completionHandler: { image, _ in + imageView.image = image + }) + return imageView } } diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index cb61825774e..4b67b6849a3 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -9,7 +9,6 @@ import TelegramPresentationData import TelegramUIPreferences import PresentationDataUtils import AccountContext -import WebKit import AppBundle import PromptUI import SafariServices @@ -18,11 +17,12 @@ import UndoUI import UrlEscaping import PDFKit -final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { +final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDFDocumentDelegate { private let context: AccountContext private var presentationData: PresentationData + let file: TelegramMediaFile - private let webView: PDFView + private let pdfView: PDFView private let scrollView: UIScrollView! let uuid: UUID @@ -52,14 +52,12 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.context = context self.uuid = UUID() self.presentationData = presentationData + self.file = file - self.webView = PDFView() - self.webView.maxScaleFactor = 4.0; - self.webView.minScaleFactor = self.webView.scaleFactorForSizeToFit - self.webView.autoScales = true - + self.pdfView = PDFView() + var scrollView: UIScrollView? - for view in self.webView.subviews { + for view in self.pdfView.subviews { if let view = view as? UIScrollView { scrollView = view } else { @@ -72,21 +70,24 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } self.scrollView = scrollView - var title: String = "file" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { -// var updatedPath = path -// if let fileName = file.fileName { -// let tempFile = TempBox.shared.file(path: path, fileName: fileName) -// updatedPath = tempFile.path -// self.tempFile = tempFile -// title = fileName -// } - - self.webView.document = PDFDocument(data: data) - title = file.fileName ?? "file" + self.pdfView.displayDirection = .vertical + self.pdfView.autoScales = true + + var title = "file" + var url = "" + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + var updatedPath = path + if let fileName = file.fileName { + let tempFile = TempBox.shared.file(path: path, fileName: fileName) + updatedPath = tempFile.path + self.tempFile = tempFile + title = fileName + url = updatedPath + } + self.pdfView.document = PDFDocument(url: URL(fileURLWithPath: updatedPath)) } - self._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) self.statePromise = Promise(self._state) super.init(frame: .zero) @@ -94,10 +95,12 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU if #available(iOS 15.0, *) { self.backgroundColor = presentationData.theme.list.plainBackgroundColor } - self.addSubview(self.webView) + self.addSubview(self.pdfView) Queue.mainQueue().after(1.0) { - scrollView?.delegate = self + if let scrollView = self.scrollView { + scrollView.delegate = self + } } } @@ -115,134 +118,171 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } - var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false) + func updateFontState(_ state: BrowserPresentationState.FontState) { - self.updateFontState(state, force: false) + } func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) { - self.currentFontState = state + } -// let fontFamily = state.isSerif ? "'Georgia, serif'" : "null" -// let textSizeAdjust = state.size != 100 ? "'\(state.size)%'" : "null" -// let js = "\(setupFontFunctions) setTelegramFontOverrides(\(fontFamily), \(textSizeAdjust))"; -// self.webView.evaluateJavaScript(js) { _, _ in } - } - - private var didSetupSearch = false - private func setupSearch(completion: @escaping () -> Void) { -// guard !self.didSetupSearch else { -// completion() -// return -// } -// -// let bundle = getAppBundle() -// guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else { -// return -// } -// guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else { -// return -// } -// guard let script = String(data: scriptData, encoding: .utf8) else { -// return -// } -// self.didSetupSearch = true -// self.webView.evaluateJavaScript(script, completionHandler: { _, error in -// if error != nil { -// print() -// } -// completion() -// }) + private var findSession: Any? + private var previousQuery: String? + private var currentSearchResult: Int = 0 + private var searchResultsCount: Int = 0 + private var searchResults: [PDFSelection] = [] + private var searchCompletion: ((Int) -> Void)? + + private let matchColor = UIColor(rgb: 0xd4d4d, alpha: 0.2) + private let selectedColor = UIColor(rgb: 0xffe438) + + func didMatchString(_ instance: PDFSelection) { + instance.color = self.matchColor + self.searchResults.append(instance) } - private var previousQuery: String? - func setSearch(_ query: String?, completion: ((Int) -> Void)?) { -// guard self.previousQuery != query else { -// return -// } -// self.previousQuery = query -// self.setupSearch { [weak self] in -// if let query = query { -// let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')" -// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in -// let js = "uiWebview_SearchResultCount" -// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in -// if let result = result as? NSNumber { -// self?.searchResultsCount = result.intValue -// completion?(result.intValue) -// } else { -// completion?(0) -// } -// }) -// }) -// } else { -// let js = "uiWebview_RemoveAllHighlights()" -// self?.webView.evaluateJavaScript(js, completionHandler: nil) -// -// self?.currentSearchResult = 0 -// self?.searchResultsCount = 0 -// } -// } + func documentDidEndDocumentFind(_ notification: Notification) { + self.searchResultsCount = self.searchResults.count + + if let searchCompletion = self.searchCompletion { + self.searchCompletion = nil + searchCompletion(self.searchResultsCount) + } + + self.updateSearchHighlights(highlightedSelection: self.searchResults.first) } - private var currentSearchResult: Int = 0 - private var searchResultsCount: Int = 0 + func updateSearchHighlights(highlightedSelection: PDFSelection?) { + self.pdfView.highlightedSelections = nil + if let highlightedSelection { + for selection in self.searchResults { + if selection === highlightedSelection { + selection.color = self.selectedColor + } else { + selection.color = self.matchColor + } + } + self.pdfView.highlightedSelections = self.searchResults + } + } + + func setSearch(_ query: String?, completion: ((Int) -> Void)?) { + guard let document = self.pdfView.document, self.previousQuery != query else { + return + } + self.previousQuery = query + + if #available(iOS 16.0, *), !"".isEmpty { + if let query { + var findSession: UIFindSession? + if let current = self.findSession as? UIFindSession { + findSession = current + } else { + self.pdfView.isFindInteractionEnabled = true + + if let session = self.pdfView.findInteraction(self.pdfView.findInteraction, sessionFor: self.pdfView) { + findSession = session + self.findSession = session + + self.pdfView.findInteraction(self.pdfView.findInteraction, didBegin: session) + } + } + if let findSession { + findSession.performSearch(query: query, options: BrowserSearchOptions()) + self.pdfView.findInteraction.updateResultCount() + completion?(findSession.resultCount) + } + } else { + if let session = self.findSession as? UIFindSession { + self.pdfView.findInteraction(self.pdfView.findInteraction, didEnd: session) + self.findSession = nil + self.pdfView.isFindInteractionEnabled = false + } + } + } else { + if let query { + self.currentSearchResult = 0 + self.searchCompletion = completion + + document.cancelFindString() + document.delegate = self + document.beginFindString(query, withOptions: .caseInsensitive) + } else { + self.searchResults = [] + self.currentSearchResult = 0 + self.searchResultsCount = 0 + + self.updateSearchHighlights(highlightedSelection: nil) + + document.cancelFindString() + document.delegate = nil + + completion?(0) + } + } + } func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { -// let searchResultsCount = self.searchResultsCount -// var index = self.currentSearchResult - 1 -// if index < 0 { -// index = searchResultsCount - 1 -// } -// self.currentSearchResult = index -// -// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" -// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in -// completion?(index, searchResultsCount) -// }) + if #available(iOS 16.0, *), !"".isEmpty { + if let session = self.findSession as? UIFindSession { + session.highlightNextResult(in: .backward) + completion?(session.highlightedResultIndex, session.resultCount) + } + } else { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult - 1 + if index < 0 { + index = searchResultsCount - 1 + } + self.currentSearchResult = index + + if index >= 0 && index < self.searchResults.count { + self.updateSearchHighlights(highlightedSelection: self.searchResults[index]) + + self.pdfView.go(to: self.searchResults[index]) + completion?(index, searchResultsCount) + } + } } func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { -// let searchResultsCount = self.searchResultsCount -// var index = self.currentSearchResult + 1 -// if index >= searchResultsCount { -// index = 0 -// } -// self.currentSearchResult = index -// -// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')" -// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in -// completion?(index, searchResultsCount) -// }) + if #available(iOS 16.0, *), !"".isEmpty { + if let session = self.findSession as? UIFindSession { + session.highlightNextResult(in: .forward) + completion?(session.highlightedResultIndex, session.resultCount) + } + } else { + let searchResultsCount = self.searchResultsCount + var index = self.currentSearchResult + 1 + if index >= searchResultsCount { + index = 0 + } + self.currentSearchResult = index + + if index >= 0 && index < self.searchResults.count { + self.updateSearchHighlights(highlightedSelection: self.searchResults[index]) + + self.pdfView.go(to: self.searchResults[index]) + completion?(index, searchResultsCount) + } + } } func stop() { -// self.webView.stopLoading() } func reload() { -// self.webView.reload() } func navigateBack() { -// self.webView.goBack() } func navigateForward() { -// self.webView.goForward() } func navigateTo(historyItem: BrowserContentState.HistoryItem) { -// if let webItem = historyItem.webItem { -// self.webView.go(to: webItem) -// } } func navigateTo(address: String) { -// let finalUrl = explicitUrl(address) -// guard let url = URL(string: finalUrl) else { -// return -// } -// self.webView.load(URLRequest(url: url)) } func scrollToTop() { @@ -251,42 +291,19 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU private var validLayout: (CGSize, UIEdgeInsets, UIEdgeInsets)? func updateLayout(size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, transition: ComponentTransition) { + let isFirstTime = self.validLayout == nil self.validLayout = (size, insets, fullInsets) self.previousScrollingOffset = ScrollingOffsetState(value: self.scrollView.contentOffset.y, isDraggingOrDecelerating: self.scrollView.isDragging || self.scrollView.isDecelerating) - let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - insets.bottom)) - var refresh = false - if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width { - refresh = true - } - transition.setFrame(view: self.webView, frame: webViewFrame) + let pdfViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - insets.bottom)) + transition.setFrame(view: self.pdfView, frame: pdfViewFrame) - if refresh { - self.webView.reloadInputViews() + if isFirstTime { + self.pdfView.setNeedsLayout() + self.pdfView.layoutIfNeeded() + self.pdfView.minScaleFactor = self.pdfView.scaleFactorForSizeToFit } - -// if let error = self.currentError { -// let errorSize = self.errorView.update( -// transition: .immediate, -// component: AnyComponent( -// ErrorComponent( -// theme: self.presentationData.theme, -// title: self.presentationData.strings.Browser_ErrorTitle, -// text: error.localizedDescription -// ) -// ), -// environment: {}, -// containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height) -// ) -// if self.errorView.superview == nil { -// self.addSubview(self.errorView) -// self.errorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) -// } -// self.errorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - errorSize.width) / 2.0), y: insets.top + floorToScreenPixels((size.height - insets.top - insets.bottom - errorSize.height) / 2.0)), size: errorSize) -// } else if self.errorView.superview != nil { -// self.errorView.removeFromSuperview() -// } } private func updateState(_ f: (BrowserContentState) -> BrowserContentState) { @@ -302,22 +319,64 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU private var previousScrollingOffset: ScrollingOffsetState? - func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateScrollingOffset(isReset: false, transition: .immediate) - } - private func snapScrollingOffsetToInsets() { let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)) self.updateScrollingOffset(isReset: false, transition: transition) } + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + if let scrollViewDelegate = scrollView as? UIScrollViewDelegate { + return scrollViewDelegate.viewForZooming?(in: scrollView) + } + return nil + } + + func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { + if let scrollViewDelegate = scrollView as? UIScrollViewDelegate { + scrollViewDelegate.scrollViewWillBeginZooming?(scrollView, with: view) + } + self.resetScrolling() + self.wasZooming = true + } + + private var wasZooming = false + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + if let scrollViewDelegate = scrollView as? UIScrollViewDelegate { + scrollViewDelegate.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) + } + Queue.mainQueue().after(0.1, { + self.wasZooming = false + }) + } + + func scrollViewDidZoom(_ scrollView: UIScrollView) { + if let scrollViewDelegate = scrollView as? UIScrollViewDelegate { + scrollViewDelegate.scrollViewDidZoom?(scrollView) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let scrollViewDelegate = scrollView as? UIScrollViewDelegate { + scrollViewDelegate.scrollViewDidScroll?(scrollView) + } + if !scrollView.isZooming && !self.wasZooming { + self.updateScrollingOffset(isReset: false, transition: .immediate) + } + } + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if let scrollViewDelegate = scrollView as? UIScrollViewDelegate { + scrollViewDelegate.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) + } if !decelerate { self.snapScrollingOffsetToInsets() } } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + if let scrollViewDelegate = scrollView as? UIScrollViewDelegate { + scrollViewDelegate.scrollViewDidEndDecelerating?(scrollView) + } self.snapScrollingOffsetToInsets() } @@ -357,90 +416,6 @@ final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.updateScrollingOffset(isReset: true, transition: .spring(duration: 0.4)) } - func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { -// self.currentError = nil - self.updateFontState(self.currentFontState, force: true) - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - self.updateState { - $0 - .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) - .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) - } - } - -// func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { -// if (error as NSError).code != -999 { -// self.currentError = error -// } else { -// self.currentError = nil -// } -// if let (size, insets) = self.validLayout { -// self.updateLayout(size: size, insets: insets, transition: .immediate) -// } -// } -// -// func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { -// if (error as NSError).code != -999 { -// self.currentError = error -// } else { -// self.currentError = nil -// } -// if let (size, insets) = self.validLayout { -// self.updateLayout(size: size, insets: insets, transition: .immediate) -// } -// } - - func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { - if navigationAction.targetFrame == nil { - if let url = navigationAction.request.url?.absoluteString { - self.open(url: url, new: true) - } - } - return nil - } - - func webViewDidClose(_ webView: WKWebView) { - self.close() - } - - @available(iOSApplicationExtension 15.0, iOS 15.0, *) - func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { - decisionHandler(.prompt) - } - - -// @available(iOS 13.0, *) -// func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { -// guard let url = elementInfo.linkURL else { -// completionHandler(nil) -// return -// } -// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } -// let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in -// return UIMenu(title: "", children: [ -// UIAction(title: presentationData.strings.Browser_ContextMenu_Open, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// self?.open(url: url.absoluteString, new: false) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_OpenInNewTab, image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// self?.open(url: url.absoluteString, new: true) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_AddToReadingList, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in -// let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// UIPasteboard.general.string = url.absoluteString -// self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) -// }), -// UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in -// self?.share(url: url.absoluteString) -// }) -// ]) -// } -// completionHandler(configuration) -// } - private func open(url: String, new: Bool) { let subject: BrowserScreen.Subject = .webPage(url: url) if new, let navigationController = self.getNavigationController() { diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 70fd9ebdc3e..16bd42554cc 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -28,6 +28,7 @@ private final class BrowserScreenComponent: CombinedComponent { let context: AccountContext let contentState: BrowserContentState? let presentationState: BrowserPresentationState + let canShare: Bool let performAction: ActionSlot let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void let panelCollapseFraction: CGFloat @@ -36,6 +37,7 @@ private final class BrowserScreenComponent: CombinedComponent { context: AccountContext, contentState: BrowserContentState?, presentationState: BrowserPresentationState, + canShare: Bool, performAction: ActionSlot, performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void, panelCollapseFraction: CGFloat @@ -43,6 +45,7 @@ private final class BrowserScreenComponent: CombinedComponent { self.context = context self.contentState = contentState self.presentationState = presentationState + self.canShare = canShare self.performAction = performAction self.performHoldAction = performHoldAction self.panelCollapseFraction = panelCollapseFraction @@ -58,6 +61,9 @@ private final class BrowserScreenComponent: CombinedComponent { if lhs.presentationState != rhs.presentationState { return false } + if lhs.canShare != rhs.canShare { + return false + } if lhs.panelCollapseFraction != rhs.panelCollapseFraction { return false } @@ -260,25 +266,27 @@ private final class BrowserScreenComponent: CombinedComponent { ), at: 0 ) - navigationRightItems.insert( - AnyComponentWithIdentity( - id: "share", - component: AnyComponent( - Button( - content: AnyComponent( - BundleIconComponent( - name: "Chat List/NavigationShare", - tintColor: environment.theme.rootController.navigationBar.accentTextColor - ) - ), - action: { - performAction.invoke(.share) - } + if context.component.canShare { + navigationRightItems.insert( + AnyComponentWithIdentity( + id: "share", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Chat List/NavigationShare", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) + ), + action: { + performAction.invoke(.share) + } + ) ) - ) - ), - at: 0 - ) + ), + at: 0 + ) + } if canOpenIn { navigationRightItems.append( AnyComponentWithIdentity( @@ -359,6 +367,8 @@ private final class BrowserScreenComponent: CombinedComponent { canGoBack: context.component.contentState?.canGoBack ?? false, canGoForward: context.component.contentState?.canGoForward ?? false, canOpenIn: canOpenIn, + canShare: context.component.canShare, + isDocument: context.component.contentState?.contentType == .document, performAction: performAction, performHoldAction: performHoldAction ) @@ -552,7 +562,22 @@ public class BrowserScreen: ViewController, MinimizableController { content.navigateForward() case .share: let presentationData = self.presentationData - let shareController = ShareController(context: self.context, subject: .url(url)) + let subject: ShareControllerSubject + var isDocument = false + if let content = self.content.last { + if let documentContent = content as? BrowserDocumentContent { + subject = .media(.standalone(media: documentContent.file)) + isDocument = true + } else if let documentContent = content as? BrowserPdfContent { + subject = .media(.standalone(media: documentContent.file)) + isDocument = true + } else { + subject = .url(url) + } + } else { + subject = .url(url) + } + let shareController = ShareController(context: self.context, subject: subject) shareController.completed = { [weak self] peerIds in guard let strongSelf = self else { return @@ -572,20 +597,21 @@ public class BrowserScreen: ViewController, MinimizableController { let text: String var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId && !isDocument { text = presentationData.strings.WebBrowser_LinkAddedToBookmarks savedMessages = true } else { if peers.count == 1, let peer = peers.first { let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.WebBrowser_LinkForwardTooltip_Chat_One(peerName).string + text = isDocument ? presentationData.strings.WebBrowser_FileForwardTooltip_Chat_One(peerName).string : presentationData.strings.WebBrowser_LinkForwardTooltip_Chat_One(peerName).string + savedMessages = peer.id == strongSelf.context.account.peerId } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.WebBrowser_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + text = isDocument ? presentationData.strings.WebBrowser_FileForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.WebBrowser_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string } else if let peer = peers.first { let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.WebBrowser_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + text = isDocument ? presentationData.strings.WebBrowser_FileForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string : presentationData.strings.WebBrowser_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string } else { text = "" } @@ -616,7 +642,19 @@ public class BrowserScreen: ViewController, MinimizableController { case .minimize: self.minimize() case .openIn: - self.context.sharedContext.applicationBindings.openUrl(url) + var processed = false + if let controller = self.controller { + switch controller.subject { + case let .document(file, canShare), let .pdfDocument(file, canShare): + processed = true + controller.openDocument(file, canShare) + default: + break + } + } + if !processed { + self.context.sharedContext.applicationBindings.openUrl(url) + } case .openSettings: self.openSettings() case let .updateSearchActive(active): @@ -640,6 +678,7 @@ public class BrowserScreen: ViewController, MinimizableController { }) }) case .scrollToPreviousSearchResult: + self.view.window?.endEditing(true) content.scrollToPreviousSearchResult(completion: { [weak self] index, count in self?.updatePresentationState({ state in var updatedState = state @@ -649,6 +688,7 @@ public class BrowserScreen: ViewController, MinimizableController { }) }) case .scrollToNextSearchResult: + self.view.window?.endEditing(true) content.scrollToNextSearchResult(completion: { [weak self] index, count in self?.updatePresentationState({ state in var updatedState = state @@ -803,9 +843,9 @@ public class BrowserScreen: ViewController, MinimizableController { self.openPeer(peer) } browserContent = instantPageContent - case let .document(file): + case let .document(file, _): browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file) - case let .pdfDocument(file): + case let .pdfDocument(file, _): browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: file) } browserContent.pushContent = { [weak self] content in @@ -1084,24 +1124,37 @@ public class BrowserScreen: ViewController, MinimizableController { openInUrl = url } - let canOpenIn = !(self.contentState?.url.hasPrefix("tonsite") ?? false) + var canShare = true + if let controller = self.controller { + switch controller.subject { + case let .document(_, canShareValue), let .pdfDocument(_, canShareValue): + canShare = canShareValue + default: + break + } + } var items: [ContextMenuItem] = [] - items.append(.custom(fontItem, false)) - + if contentState.contentType == .document, contentState.title.lowercased().hasSuffix(".pdf") { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in + } else { + items.append(.custom(fontItem, false)) + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in performAction.invoke(.updateFontIsSerif(false)) action(.default) - }))) - - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in + }))) + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in performAction.invoke(.updateFontIsSerif(true)) action(.default) - }))) + }))) + } - items.append(.separator) + if !items.isEmpty { + items.append(.separator) + } if case .webPage = contentState.contentType { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Reload, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Reload"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in @@ -1109,14 +1162,14 @@ public class BrowserScreen: ViewController, MinimizableController { action(.default) }))) } - if [.webPage].contains(contentState.contentType) { + if [.webPage, .document].contains(contentState.contentType) { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.updateSearchActive(true)) action(.default) }))) } - if !layout.metrics.isTablet { + if canShare && !layout.metrics.isTablet { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.share) action(.default) @@ -1287,6 +1340,16 @@ public class BrowserScreen: ViewController, MinimizableController { return self?.controller } ) + + var canShare = true + if let controller = self.controller { + switch controller.subject { + case let .document(_, canShareValue), let .pdfDocument(_, canShareValue): + canShare = canShareValue + default: + break + } + } let componentSize = self.componentHost.update( transition: transition, @@ -1295,6 +1358,7 @@ public class BrowserScreen: ViewController, MinimizableController { context: self.context, contentState: self.contentState, presentationState: self.presentationState, + canShare: canShare, performAction: self.performAction, performHoldAction: { [weak self] view, gesture, action in if let self { @@ -1371,27 +1435,40 @@ public class BrowserScreen: ViewController, MinimizableController { public enum Subject { case webPage(url: String) case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) - case document(file: TelegramMediaFile) - case pdfDocument(file: TelegramMediaFile) + case document(file: TelegramMediaFile, canShare: Bool) + case pdfDocument(file: TelegramMediaFile, canShare: Bool) } private let context: AccountContext - private let subject: Subject + fileprivate let subject: Subject private var preferredConfiguration: WKWebViewConfiguration? private var openPreviousOnClose = false + public var openDocument: (TelegramMediaFile, Bool) -> Void = { _, _ in } + private var validLayout: ContainerViewLayout? public static let supportedDocumentMimeTypes: [String] = [ -// "text/plain", -// "text/rtf", -// "application/pdf", -// "application/msword", -// "application/vnd.openxmlformats-officedocument.wordprocessingml.document", -// "application/vnd.ms-excel", -// "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", -// "application/vnd.openxmlformats-officedocument.spreadsheetml.template", -// "application/vnd.openxmlformats-officedocument.presentationml.presentation" + "text/plain", + "text/rtf", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "application/vnd.openxmlformats-officedocument.presentationml.presentation" + ] + + public static let supportedDocumentExtensions: [String] = [ + "txt", + "rtf", + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "pptx" ] public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) { diff --git a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift index 6e8974667fa..9678ab3b060 100644 --- a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import SwiftSignalKit import ComponentFlow import TelegramPresentationData import AccountContext @@ -60,9 +61,8 @@ final class SearchBarContentComponent: Component { } } - private let activated: (Bool) -> Void = { _ in } - private let deactivated: (Bool) -> Void = { _ in } - private let updateQuery: (String?) -> Void = { _ in } + private let queryPromise = ValuePromise() + private var queryDisposable: Disposable? private let backgroundLayer: SimpleLayer @@ -84,10 +84,6 @@ final class SearchBarContentComponent: Component { private var params: Params? private var component: SearchBarContentComponent? - public var wantsDisplayBelowKeyboard: Bool { - return self.textField != nil - } - init() { self.backgroundLayer = SimpleLayer() @@ -145,6 +141,23 @@ final class SearchBarContentComponent: Component { } } self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside) + + let throttledSearchQuery = self.queryPromise.get() + |> mapToSignal { query -> Signal in + if !query.isEmpty { + return (.complete() |> delay(0.6, queue: Queue.mainQueue())) + |> then(.single(query)) + } else { + return .single(query) + } + } + + self.queryDisposable = (throttledSearchQuery + |> deliverOnMainQueue).start(next: { [weak self] query in + if let self { + self.component?.performAction.invoke(.updateSearchQuery(query)) + } + }) } required public init?(coder: NSCoder) { @@ -160,9 +173,10 @@ final class SearchBarContentComponent: Component { private func activateTextInput() { if self.textField == nil, let textFrame = self.textFrame { let backgroundFrame = self.backgroundLayer.frame - let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height)) + let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX - 32.0, height: backgroundFrame.height)) let textField = SearchTextField(frame: textFieldFrame) + textField.clipsToBounds = true textField.autocorrectionType = .no textField.returnKeyType = .search self.textField = textField @@ -174,23 +188,17 @@ final class SearchBarContentComponent: Component { guard !(self.textField?.isFirstResponder ?? false) else { return } - - self.activated(true) - + self.textField?.becomeFirstResponder() } @objc private func cancelPressed() { - self.updateQuery(nil) - self.clearIconView.isHidden = true self.clearIconButton.isHidden = true let textField = self.textField self.textField = nil - - self.deactivated(textField?.isFirstResponder ?? false) - + self.component?.performAction.invoke(.updateSearchActive(false)) if let textField { @@ -200,11 +208,11 @@ final class SearchBarContentComponent: Component { } @objc private func clearPressed() { - self.updateQuery(nil) - self.textField?.text = "" - - self.clearIconView.isHidden = true - self.clearIconButton.isHidden = true + guard let textField = self.textField else { + return + } + textField.text = "" + self.textFieldChanged(textField) } func deactivate() { @@ -232,10 +240,8 @@ final class SearchBarContentComponent: Component { self.clearIconView.isHidden = text.isEmpty self.clearIconButton.isHidden = text.isEmpty self.placeholderContent.view?.isHidden = !text.isEmpty - - self.updateQuery(text) - - self.component?.performAction.invoke(.updateSearchQuery(text)) + + self.queryPromise.set(text) if let params = self.params { self.update(theme: params.theme, strings: params.strings, size: params.size, transition: .immediate) diff --git a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift index 4c1e2c5a32c..a359f7c9e8f 100644 --- a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift @@ -125,6 +125,8 @@ final class NavigationToolbarContentComponent: CombinedComponent { let canGoBack: Bool let canGoForward: Bool let canOpenIn: Bool + let canShare: Bool + let isDocument: Bool let performAction: ActionSlot let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void @@ -134,6 +136,8 @@ final class NavigationToolbarContentComponent: CombinedComponent { canGoBack: Bool, canGoForward: Bool, canOpenIn: Bool, + canShare: Bool, + isDocument: Bool, performAction: ActionSlot, performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void ) { @@ -142,6 +146,8 @@ final class NavigationToolbarContentComponent: CombinedComponent { self.canGoBack = canGoBack self.canGoForward = canGoForward self.canOpenIn = canOpenIn + self.canShare = canShare + self.isDocument = isDocument self.performAction = performAction self.performHoldAction = performHoldAction } @@ -162,6 +168,12 @@ final class NavigationToolbarContentComponent: CombinedComponent { if lhs.canOpenIn != rhs.canOpenIn { return false } + if lhs.canShare != rhs.canShare { + return false + } + if lhs.isDocument != rhs.isDocument { + return false + } return true } @@ -171,6 +183,8 @@ final class NavigationToolbarContentComponent: CombinedComponent { let share = Child(Button.self) let bookmark = Child(Button.self) let openIn = Child(Button.self) + let search = Child(Button.self) + let quickLook = Child(Button.self) return { context in let availableSize = context.availableSize @@ -180,69 +194,17 @@ final class NavigationToolbarContentComponent: CombinedComponent { let sideInset: CGFloat = 5.0 let buttonSize = CGSize(width: 50.0, height: availableSize.height) - var buttonCount = 4 + var buttonCount = 3 + if context.component.canShare { + buttonCount += 1 + } if context.component.canOpenIn { buttonCount += 1 } let spacing = (availableSize.width - buttonSize.width * CGFloat(buttonCount) - sideInset * 2.0) / CGFloat(buttonCount - 1) - let canGoBack = context.component.canGoBack - let back = back.update( - component: ContextReferenceButtonComponent( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Back", - tintColor: canGoBack ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) - ) - ), - minSize: buttonSize, - action: { view, gesture in - guard canGoBack else { - return - } - if let gesture { - performHoldAction(view, gesture, .navigateBack) - } else { - performAction.invoke(.navigateBack) - } - } - ), - availableSize: buttonSize, - transition: .easeInOut(duration: 0.2) - ) - context.add(back - .position(CGPoint(x: sideInset + back.size.width / 2.0, y: availableSize.height / 2.0)) - ) - - let canGoForward = context.component.canGoForward - let forward = forward.update( - component: ContextReferenceButtonComponent( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Forward", - tintColor: canGoForward ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) - ) - ), - minSize: buttonSize, - action: { view, gesture in - guard canGoForward else { - return - } - if let gesture { - performHoldAction(view, gesture, .navigateForward) - } else { - performAction.invoke(.navigateForward) - } - } - ), - availableSize: buttonSize, - transition: .easeInOut(duration: 0.2) - ) - context.add(forward - .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width / 2.0, y: availableSize.height / 2.0)) - ) - + let canShare = context.component.canShare let share = share.update( component: Button( content: AnyComponent( @@ -252,41 +214,50 @@ final class NavigationToolbarContentComponent: CombinedComponent { ) ), action: { - performAction.invoke(.share) - } - ).minSize(buttonSize), - availableSize: buttonSize, - transition: .easeInOut(duration: 0.2) - ) - context.add(share - .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0)) - ) - - let bookmark = bookmark.update( - component: Button( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Bookmark", - tintColor: context.component.accentColor - ) - ), - action: { - performAction.invoke(.openBookmarks) + if canShare { + performAction.invoke(.share) + } } ).minSize(buttonSize), availableSize: buttonSize, transition: .easeInOut(duration: 0.2) ) - context.add(bookmark - .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width / 2.0, y: availableSize.height / 2.0)) - ) - if context.component.canOpenIn { - let openIn = openIn.update( + if context.component.isDocument { + if !context.component.canShare { + context.add(share + .position(CGPoint(x: availableSize.width / 2.0, y: 10000.0)) + ) + } else { + context.add(share + .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + ) + } + + let search = search.update( component: Button( content: AnyComponent( BundleIconComponent( - name: "Instant View/Browser", + name: "Chat List/SearchIcon", + tintColor: context.component.accentColor + ) + ), + action: { + performAction.invoke(.updateSearchActive(true)) + } + ).minSize(buttonSize), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(search + .position(CGPoint(x: sideInset + search.size.width / 2.0, y: availableSize.height / 2.0)) + ) + + let quickLook = quickLook.update( + component: Button( + content: AnyComponent( + BundleIconComponent( + name: "Stories/EmbeddedViewIcon", tintColor: context.component.accentColor ) ), @@ -297,9 +268,109 @@ final class NavigationToolbarContentComponent: CombinedComponent { availableSize: buttonSize, transition: .easeInOut(duration: 0.2) ) - context.add(openIn - .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0)) + context.add(quickLook + .position(CGPoint(x: context.availableSize.width - sideInset - quickLook.size.width / 2.0, y: availableSize.height / 2.0)) + ) + } else { + let canGoBack = context.component.canGoBack + let back = back.update( + component: ContextReferenceButtonComponent( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Back", + tintColor: canGoBack ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) + ) + ), + minSize: buttonSize, + action: { view, gesture in + guard canGoBack else { + return + } + if let gesture { + performHoldAction(view, gesture, .navigateBack) + } else { + performAction.invoke(.navigateBack) + } + } + ), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(back + .position(CGPoint(x: sideInset + back.size.width / 2.0, y: availableSize.height / 2.0)) + ) + + let canGoForward = context.component.canGoForward + let forward = forward.update( + component: ContextReferenceButtonComponent( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Forward", + tintColor: canGoForward ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) + ) + ), + minSize: buttonSize, + action: { view, gesture in + guard canGoForward else { + return + } + if let gesture { + performHoldAction(view, gesture, .navigateForward) + } else { + performAction.invoke(.navigateForward) + } + } + ), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(forward + .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width / 2.0, y: availableSize.height / 2.0)) + ) + + context.add(share + .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0)) + ) + + let bookmark = bookmark.update( + component: Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Bookmark", + tintColor: context.component.accentColor + ) + ), + action: { + performAction.invoke(.openBookmarks) + } + ).minSize(buttonSize), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(bookmark + .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width / 2.0, y: availableSize.height / 2.0)) ) + + if context.component.canOpenIn { + let openIn = openIn.update( + component: Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Browser", + tintColor: context.component.accentColor + ) + ), + action: { + performAction.invoke(.openIn) + } + ).minSize(buttonSize), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(openIn + .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0)) + ) + } } return availableSize diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index d34787b4b49..8b6e55048a2 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -282,12 +282,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor - self.webView.isOpaque = false + self.webView.alpha = 0.0 self.webView.allowsBackForwardNavigationGestures = true self.webView.scrollView.delegate = self self.webView.scrollView.clipsToBounds = false -// self.webView.translatesAutoresizingMaskIntoConstraints = false + self.webView.navigationDelegate = self self.webView.uiDelegate = self self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil) @@ -378,7 +378,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } } - var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false) func updateFontState(_ state: BrowserPresentationState.FontState) { self.updateFontState(state, force: false) @@ -476,6 +475,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self?.currentSearchResult = 0 self?.searchResultsCount = 0 + completion?(0) } } } @@ -630,6 +630,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } self.didSetupSearch = false } else if keyPath == "estimatedProgress" { + if self.webView.estimatedProgress >= 0.1 && self.webView.alpha.isZero { + self.webView.alpha = 1.0 + self.webView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } } else if keyPath == "canGoBack" { self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } @@ -739,7 +743,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.minimize() self.openAppUrl(url) } else { - decisionHandler(.allow, preferences) + if let scheme = navigationAction.request.url?.scheme, !["http", "https", "tonsite", "about"].contains(scheme.lowercased()) { + decisionHandler(.cancel, preferences) + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: {}) + } else { + decisionHandler(.allow, preferences) + } } } else { decisionHandler(.allow, preferences) @@ -776,7 +785,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU decisionHandler(.allow) } } - + func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { if let _ = self.currentError { self.currentError = nil @@ -788,10 +797,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - self.updateState { - $0 - .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) - .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) + self.updateState {$0 + .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) + .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) } self.parseFavicon() } diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 5935c17f1f4..29b5da4e424 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -8,10 +8,10 @@ NGDEPS = [ "//Nicegram/NGTranslate:NGTranslate", # Nicegram downloading feature "//submodules/SaveToCameraRoll:SaveToCameraRoll", + "@swiftpkg_nicegram_assistant_ios//:FeatAssistant", "@swiftpkg_nicegram_assistant_ios//:FeatHiddenChats", "@swiftpkg_nicegram_assistant_ios//:FeatPinnedChats", "@swiftpkg_nicegram_assistant_ios//:NGAiChat", - "@swiftpkg_nicegram_assistant_ios//:NGAssistantUI", "@swiftpkg_nicegram_assistant_ios//:NGRepoTg", "@swiftpkg_nicegram_assistant_ios//:NGRepoUser", "@swiftpkg_nicegram_assistant_ios//:NGSpecialOffer", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5bd1d40f436..b235e1e6d2e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2,13 +2,12 @@ import Foundation import UIKit import Postbox // MARK: Nicegram Imports +import FeatAssistant import NGAiChat import NGAiChatUI import NGAnalytics import NGData import NGAppCache -import NGAssistant -import NGAssistantUI import NGCore import FeatAuth import NGModels @@ -352,7 +351,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController component: AnyComponent(NicegramButtonComponent( pressed: { Task { - AssistantUITgHelper.presentAssistantModally( + AssistantTgHelper.presentAssistantModally( source: .navigationBarIcon ) } @@ -1233,6 +1232,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.openBirthdaySetup() } + + self.chatListDisplayNode.mainContainerNode.openStarsTopup = { [weak self] amount in + guard let self else { + return + } + self.openStarsTopup(amount: amount) + } + self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in guard let self else { return @@ -6143,6 +6150,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.push(controller) } + func openStarsTopup(amount: Int64?) { + guard let starsContext = self.context.starsContext else { + return + } + let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: [], purpose: amount.flatMap({ .topUp(requiredStars: $0, purpose: "subs") }) ?? .generic, completion: { _ in }) + self.push(controller) + } + private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator? var hasStoryCameraTransition: Bool { return self.storyCameraTransitionInCoordinator != nil diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 64cb44c6be3..790b0aa7b4c 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -351,6 +351,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele itemNode.listNode.openPremiumManagement = { [weak self] in self?.openPremiumManagement?() } + itemNode.listNode.openStarsTopup = { [weak self] amount in + self?.openStarsTopup?(amount) + } self.currentItemStateValue.set(itemNode.listNode.state |> map { state in let filterId: Int32? @@ -416,12 +419,28 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele var openBirthdaySetup: (() -> Void)? var openPremiumManagement: (() -> Void)? var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)? + var openStarsTopup: ((Int64?) -> Void)? var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? var didBeginSelectingChats: (() -> Void)? var canExpandHiddenItems: (() -> Bool)? public var displayFilterLimit: (() -> Void)? - public init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, chatListMode: ChatListNodeMode = .chatList(appendContacts: true), previewing: Bool, controlsHistoryPreload: Bool, isInlineMode: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void) { + public init( + context: AccountContext, + controller: ChatListControllerImpl?, + location: ChatListControllerLocation, + chatListMode: ChatListNodeMode = .chatList(appendContacts: true), + previewing: Bool, + controlsHistoryPreload: Bool, + isInlineMode: Bool, + presentationData: PresentationData, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + filterBecameEmpty: @escaping (ChatListFilter?) -> Void, + filterEmptyAction: @escaping (ChatListFilter?) -> Void, + secondaryEmptyAction: @escaping () -> Void, + openArchiveSettings: @escaping () -> Void) + { self.context = context self.controller = controller self.location = location diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 31d76ca9f31..5f4b035ec60 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -742,8 +742,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { if case .channels = key { headerType = .channels } else if case .apps = key { - //TODO:localize - headerType = .text("APPS", AnyHashable("apps")) + headerType = .text(strings.ChatList_Search_SectionApps, AnyHashable("apps")) } else { if filter.contains(.onlyGroups) { headerType = .chats @@ -2705,7 +2704,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { openMediaMessageImpl = { message, mode in let _ = context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message._asMessage(), standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: { interaction.dismissInput() - }, present: { c, a in + }, present: { c, a, _ in interaction.present(c, a) }, transitionNode: { messageId, media, _ in return transitionNodeImpl?(messageId, EngineMedia(media)) @@ -2825,6 +2824,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if let sourceNode = sourceNode as? ChatListItemNode { self.interaction.openStories?(id, sourceNode.avatarNode) } + }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -2861,7 +2861,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: .peer(id: message.id.peerId), chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: { interaction.dismissInput() - }, present: { c, a in + }, present: { c, a, _ in interaction.present(c, a) }, transitionNode: { messageId, media, _ in var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? @@ -3658,19 +3658,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } else if case .apps = key { if let navigationController = self.navigationController { if isRecommended { - #if DEBUG - let _ = (self.context.sharedContext.makeMiniAppListScreenInitialData(context: self.context) - |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in - guard let self, let navigationController = self.navigationController else { - return - } - navigationController.pushViewController(self.context.sharedContext.makeMiniAppListScreen(context: self.context, initialData: initialData)) - }) - #else if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.pushViewController(peerInfoScreen) } - #endif } else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { self.context.sharedContext.openWebApp( context: self.context, @@ -4659,6 +4649,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 1bd0168a826..7319da6dfbc 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -156,7 +156,8 @@ public final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, dismissNotice: { _ in }, editPeer: { _ in }) interaction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/NicegramButtonComponent.swift b/submodules/ChatListUI/Sources/NicegramButtonComponent.swift index 5e638c06dcc..d9068de2b90 100644 --- a/submodules/ChatListUI/Sources/NicegramButtonComponent.swift +++ b/submodules/ChatListUI/Sources/NicegramButtonComponent.swift @@ -1,6 +1,6 @@ import ChatListHeaderComponent import ComponentFlow -import NGAssistantUI +import FeatAssistant import UIKit @available(iOS 13.0, *) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 29fe4114748..456698e945b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1250,6 +1250,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var avatarBadgeBackground: ASImageNode? let onlineNode: PeerOnlineMarkerNode var avatarTimerBadge: AvatarBadgeView? + private var starView: StarView? let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? var verifiedIconView: ComponentHostView? @@ -1697,7 +1698,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else if peer.id.isReplies { overrideImage = .repliesIcon } else if peer.id.isAnonymousSavedMessages { - overrideImage = .anonymousSavedMessagesIcon + overrideImage = .anonymousSavedMessagesIcon(isColored: true) } else if peer.id == item.context.account.peerId && !displayAsMessage { if case .savedMessagesChats = item.chatListLocation { overrideImage = .myNotesIcon @@ -1877,6 +1878,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item, case .chatList = item.index { self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: self.onlineIsVoiceChat), color: nil, transition: transition) + self.starView?.setOutlineColor(item.presentationData.theme.chatList.itemHighlightedBackgroundColor, transition: transition) } } else { if self.highlightedBackgroundNode.supernode != nil { @@ -1895,12 +1897,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let item = self.item { let onlineIcon: UIImage? + let effectiveBackgroundColor: UIColor if item.isPinned { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } self.onlineNode.setImage(onlineIcon, color: nil, transition: transition) + self.starView?.setOutlineColor(effectiveBackgroundColor, transition: transition) } } } @@ -2226,6 +2232,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { messageText = item.presentationData.strings.ChatList_UserReacted(value).string case .custom: break + case .stars: + break } break loop } @@ -3005,6 +3013,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { titleIconsWidth += currentMutedIconImage.size.width } + var isSubscription = false var isSecret = false if !isPeerGroup { if case let .chatList(index) = item.index, index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { @@ -3049,6 +3058,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { break } } else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer { + if peer.isSubscription { + isSubscription = true + } if case let .peer(peerData) = item.content, peerData.customMessageListData?.hidePeerStatus == true { currentCredibilityIconContent = nil } else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { @@ -3726,15 +3738,39 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateSublayerTransformScale(node: strongSelf.onlineNode, scale: (1.0 - onlineInlineNavigationFraction) * 1.0 + onlineInlineNavigationFraction * 0.00001) let onlineIcon: UIImage? + let effectiveBackgroundColor: UIColor if strongSelf.reallyHighlighted { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor } else if case let .chatList(index) = item.index, index.pinningIndex != nil { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat) + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } strongSelf.onlineNode.setImage(onlineIcon, color: item.presentationData.theme.list.itemCheckColors.foregroundColor, transition: .immediate) + if isSubscription { + let starView: StarView + if let current = strongSelf.starView { + starView = current + } else { + starView = StarView() + strongSelf.starView = starView + strongSelf.view.addSubview(starView) +// strongSelf.mainContentContainerNode.view.addSubview(starView) + } + starView.outlineColor = effectiveBackgroundColor + + let starSize = CGSize(width: 20.0, height: 20.0) + let starFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - starSize.width + 1.0, y: avatarFrame.maxY - starSize.height + 1.0), size: starSize) + transition.updateFrame(view: starView, frame: starFrame) + } else if let starView = strongSelf.starView { + strongSelf.starView = nil + starView.removeFromSuperview() + } + let autoremoveTimeoutFraction: CGFloat if online { autoremoveTimeoutFraction = 0.0 @@ -4891,5 +4927,48 @@ private extension UIImage { return image } } - // + +private class StarView: UIView { + let outline = SimpleLayer() + let foreground = SimpleLayer() + + var outlineColor: UIColor = .white { + didSet { + self.outline.layerTintColor = self.outlineColor.cgColor + } + } + + override init(frame: CGRect) { + self.outline.contents = UIImage(bundleImageName: "Premium/Stars/StarMediumOutline")?.cgImage + self.foreground.contents = UIImage(bundleImageName: "Premium/Stars/StarMedium")?.cgImage + + super.init(frame: frame) + + self.layer.addSublayer(self.outline) + self.layer.addSublayer(self.foreground) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func setOutlineColor(_ color: UIColor, transition: ContainedViewLayoutTransition) { + if case let .animated(duration, curve) = transition, color != self.outlineColor { + let snapshotLayer = SimpleLayer() + snapshotLayer.layerTintColor = self.outlineColor.cgColor + snapshotLayer.contents = self.outline.contents + snapshotLayer.frame = self.outline.bounds + self.layer.insertSublayer(snapshotLayer, above: self.outline) + snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in + snapshotLayer?.removeFromSuperlayer() + }) + } + self.outlineColor = color + } + + override func layoutSubviews() { + self.outline.frame = self.bounds + self.foreground.frame = self.bounds + } +} diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 5d71569fe54..e182680bc0f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -124,6 +124,7 @@ public final class ChatListNodeInteraction { let openChatFolderUpdates: () -> Void let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void + let openStarsTopup: (Int64?) -> Void let dismissNotice: (ChatListNotice) -> Void let editPeer: (ChatListItem) -> Void @@ -182,6 +183,7 @@ public final class ChatListNodeInteraction { openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void, + openStarsTopup: @escaping (Int64?) -> Void, dismissNotice: @escaping (ChatListNotice) -> Void, editPeer: @escaping (ChatListItem) -> Void ) { @@ -227,6 +229,7 @@ public final class ChatListNodeInteraction { self.openChatFolderUpdates = openChatFolderUpdates self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories + self.openStarsTopup = openStarsTopup self.dismissNotice = dismissNotice self.editPeer = editPeer } @@ -771,6 +774,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumGift(birthdays) case .reviewLogin: break + case let .starsSubscriptionLowBalance(amount, _): + nodeInteraction?.openStarsTopup(amount) } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1112,6 +1117,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumGift(birthdays) case .reviewLogin: break + case let .starsSubscriptionLowBalance(amount, _): + nodeInteraction?.openStarsTopup(amount) } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1231,6 +1238,7 @@ public final class ChatListNode: ListView { public var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)? public var openBirthdaySetup: (() -> Void)? public var openPremiumManagement: (() -> Void)? + public var openStarsTopup: ((Int64?) -> Void)? private var theme: PresentationTheme @@ -1840,6 +1848,11 @@ public final class ChatListNode: ListView { return } self.openStories?(subject, itemNode) + }, openStarsTopup: { [weak self] amount in + guard let self else { + return + } + self.openStarsTopup?(amount) }, dismissNotice: { [weak self] notice in guard let self else { return @@ -1941,6 +1954,8 @@ public final class ChatListNode: ListView { } else { displayArchiveIntro = .single(false) } + + let starsSubscriptionsContextPromise = Promise(nil) self.updateIsMainTabDisposable = (self.isMainTab.get() |> deliverOnMainQueue).startStrict(next: { [weak self] isMainTab in @@ -1963,9 +1978,10 @@ public final class ChatListNode: ListView { twoStepData, newSessionReviews(postbox: context.account.postbox), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)), - context.account.stateManager.contactBirthdays + context.account.stateManager.contactBirthdays, + starsSubscriptionsContextPromise.get() ) - |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, birthday, birthdays -> Signal in + |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, birthday, birthdays, starsSubscriptionsContext -> Signal in if let newSessionReview = newSessionReviews.first { return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count)) } @@ -1999,7 +2015,24 @@ public final class ChatListNode: ListView { todayBirthdayPeerIds = [] } - if suggestions.contains(.gracePremium) { + if suggestions.contains(.starsSubscriptionLowBalance) { + if let starsSubscriptionsContext { + return starsSubscriptionsContext.state + |> map { state in + if state.balance > 0 && !state.subscriptions.isEmpty { + return .starsSubscriptionLowBalance( + amount: state.balance, + peers: state.subscriptions.map { $0.peer } + ) + } else { + return nil + } + } + } else { + starsSubscriptionsContextPromise.set(.single(context.engine.payments.peerStarsSubscriptionsContext(starsContext: nil, missingBalance: true))) + return .single(nil) + } + } else if suggestions.contains(.gracePremium) { return .single(.premiumGrace) } else if suggestions.contains(.setupBirthday) && birthday == nil { return .single(.setupBirthday) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 1cadf256f29..bd28cebcc21 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -96,6 +96,7 @@ public enum ChatListNotice: Equatable { case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) case premiumGrace + case starsSubscriptionLowBalance(amount: Int64, peers: [EnginePeer]) } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index b3f88f9b8cb..cb37b982214 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -11,6 +11,8 @@ import ItemListUI import Markdown import AccountContext import MergedAvatarsNode +import TextNodeWithEntities +import TextFormat class ChatListNoticeItem: ListViewItem { enum Action { @@ -83,10 +85,11 @@ private let separatorHeight = 1.0 / UIScreen.main.scale private let titleFont = Font.semibold(15.0) private let textFont = Font.regular(15.0) +private let smallTextFont = Font.regular(14.0) final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { private let contentContainer: ASDisplayNode - private let titleNode: TextNode + private let titleNode: TextNodeWithEntities private let textNode: TextNode private let arrowNode: ASImageNode private let separatorNode: ASDisplayNode @@ -112,7 +115,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { required init() { self.contentContainer = ASDisplayNode() - self.titleNode = TextNode() + self.titleNode = TextNodeWithEntities() self.textNode = TextNode() self.arrowNode = ASImageNode() self.separatorNode = ASDisplayNode() @@ -122,7 +125,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { self.contentContainer.clipsToBounds = true self.clipsToBounds = true - self.contentContainer.addSubnode(self.titleNode) + self.contentContainer.addSubnode(self.titleNode.textNode) self.contentContainer.addSubnode(self.textNode) self.contentContainer.addSubnode(self.arrowNode) @@ -152,7 +155,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { func asyncLayout() -> (_ item: ChatListNoticeItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) { let previousItem = self.item - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeOkButtonTextLayout = TextNode.asyncLayout(self.okButtonText) @@ -262,6 +265,24 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + case let .starsSubscriptionLowBalance(amount, peers): + let title: String + let text: String + let starsValue = item.strings.ChatList_SubscriptionsLowBalance_Stars(Int32(amount)) + if let peer = peers.first, peers.count == 1 { + title = item.strings.ChatList_SubscriptionsLowBalance_Single_Title(starsValue, peer.compactDisplayTitle).string + text = item.strings.ChatList_SubscriptionsLowBalance_Single_Text + } else { + title = item.strings.ChatList_SubscriptionsLowBalance_Multiple_Title(starsValue).string + text = item.strings.ChatList_SubscriptionsLowBalance_Multiple_Text + } + let attributedTitle = NSMutableAttributedString(string: "⭐️\(title)", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) + if let range = attributedTitle.string.range(of: "⭐️") { + attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedTitle.string)) + attributedTitle.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedTitle.string)) + } + titleString = attributedTitle + textString = NSAttributedString(string: text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) } var leftInset: CGFloat = sideInset @@ -291,19 +312,19 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { strongSelf.arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.theme) } - let _ = titleLayout.1() + let _ = titleLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true)) if case .center = alignment { - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size) + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size) } else { - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.0.size) + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.0.size) } let _ = textLayout.1() if case .center = alignment { - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.textNode.frame.maxY + spacing), size: textLayout.0.size) } else { - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + spacing), size: textLayout.0.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.textNode.frame.maxY + spacing), size: textLayout.0.size) } if !avatarPeers.isEmpty { @@ -339,6 +360,8 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { hasCloseButton = true } else if case .premiumGrace = item.notice { hasCloseButton = true + } else if case .starsSubscriptionLowBalance = item.notice { + hasCloseButton = true } if let okButtonLayout, let cancelButtonLayout { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 227a314ad1b..aa595927fe9 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -345,7 +345,7 @@ public struct ComponentTransition { } } - public func setPosition(view: UIView, position: CGPoint, completion: ((Bool) -> Void)? = nil) { + public func setPosition(view: UIView, position: CGPoint, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { if view.center == position { completion?(true) return @@ -364,7 +364,7 @@ public struct ComponentTransition { } view.center = position - self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) + self.animatePosition(view: view, from: previousPosition, to: view.center, delay: delay, completion: completion) } } @@ -803,8 +803,8 @@ public struct ComponentTransition { } } - public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - self.animatePosition(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + public func animatePosition(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animatePosition(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion) } public func animateBounds(view: UIView, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { @@ -819,7 +819,7 @@ public struct ComponentTransition { self.animateBoundsSize(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) } - public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) @@ -829,7 +829,7 @@ public struct ComponentTransition { to: NSValue(cgPoint: toValue), keyPath: "position", duration: duration, - delay: 0.0, + delay: delay, curve: curve, removeOnCompletion: true, additive: additive, diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD index 4738980ea9b..26fabba83a6 100644 --- a/submodules/Components/ReactionButtonListComponent/BUILD +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TextFormat:TextFormat", "//submodules/AppBundle", + "//submodules/TelegramUI/Components/AnimatedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 1e37b9b72a4..b640158a247 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -15,6 +15,7 @@ import MultiAnimationRenderer import EmojiTextAttachmentView import TextFormat import AppBundle +import AnimatedTextComponent private let tagImage: UIImage? = { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ReactionTagBackground"), color: .white)?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 15) @@ -175,6 +176,10 @@ public final class ReactionIconView: PortalSourceView { iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) animationLayer.masksToBounds = true animationLayer.cornerRadius = floor(size.width * 0.2) + case .stars: + iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) + animationLayer.masksToBounds = false + animationLayer.cornerRadius = 0.0 } transition.updateFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) @@ -207,6 +212,8 @@ public final class ReactionIconView: PortalSourceView { iconSize = CGSize(width: floor(size.width * 2.0), height: floor(size.height * 2.0)) case .custom: iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) + case .stars: + iconSize = CGSize(width: floor(size.width * 1.25), height: floor(size.height * 1.25)) } let animationLayer = InlineStickerItemLayer( @@ -234,6 +241,9 @@ public final class ReactionIconView: PortalSourceView { case .custom: animationLayer.masksToBounds = true animationLayer.cornerRadius = floor(size.width * 0.3) + case .stars: + animationLayer.masksToBounds = false + animationLayer.cornerRadius = 0.0 } animationLayer.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) @@ -766,7 +776,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let backgroundColors: ReactionButtonAsyncNode.ContainerButtonNode.Colors - if case .custom(MessageReaction.starsReactionId) = spec.component.reaction.value { + if case .stars = spec.component.reaction.value { backgroundColors = ReactionButtonAsyncNode.ContainerButtonNode.Colors( background: spec.component.chosenOrder != nil ? spec.component.colors.selectedStarsBackground : spec.component.colors.deselectedStarsBackground, foreground: spec.component.chosenOrder != nil ? spec.component.colors.selectedStarsForeground : spec.component.colors.deselectedStarsForeground, @@ -821,6 +831,14 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { private let iconImageDisposable = MetaDisposable() + private var ignoreButtonTap: Bool = false + + private var tapAnimationLink: SharedDisplayLinkDriver.Link? + private var tapAnimationValue: CGFloat = 0.0 + private var previousTapAnimationTimestamp: Double = 0.0 + private var previousTapTimestamp: Double = 0.0 + private var tapCounterView: StarsReactionCounterView? + public var activateAfterCompletion: Bool = false { didSet { if self.activateAfterCompletion { @@ -881,6 +899,20 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } } } + + self.contextGesture?.cancelGesturesOnActivation = { [weak self] in + guard let self else { + return + } + self.buttonNode.isUserInteractionEnabled = false + self.buttonNode.cancelTracking(with: nil) + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.buttonNode.isUserInteractionEnabled = true + } + } } required init?(coder aDecoder: NSCoder) { @@ -902,16 +934,107 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { guard let layout = self.layout else { return } + if self.ignoreButtonTap { + return + } layout.spec.component.action(self, layout.spec.component.reaction.value, self.containerView) + + if case .stars = layout.spec.component.reaction.value { + self.addStarsTap() + } + } + + private func addStarsTap() { + let timestamp = CACurrentMediaTime() + + self.previousTapTimestamp = timestamp + + let deltaTime = timestamp - self.previousTapAnimationTimestamp + if deltaTime < 0.4 || self.tapCounterView != nil { + self.previousTapAnimationTimestamp = timestamp + + if let superview = self.superview { + for subview in superview.subviews { + if subview !== self { + subview.layer.zPosition = 0.0 + } + } + } + self.layer.zPosition = 1.0 + + if let tapCounterView = self.tapCounterView { + tapCounterView.add() + } else { + let tapCounterView = StarsReactionCounterView(count: 2) + self.tapCounterView = tapCounterView + self.addSubview(tapCounterView) + tapCounterView.animateIn() + if let layout = self.layout { + tapCounterView.frame = CGRect(origin: CGPoint(x: layout.size.width * 0.5, y: -70.0), size: CGSize()) + } + } + } + self.tapAnimationValue = min(1.0, self.tapAnimationValue) + + if self.tapAnimationLink == nil { + self.previousTapAnimationTimestamp = timestamp + self.updateTapAnimation() + + self.tapAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in + guard let self else { + return + } + self.updateTapAnimation() + }) + } + } + + private func updateTapAnimation() { + let timestamp = CACurrentMediaTime() + let deltaTime = min(timestamp - self.previousTapAnimationTimestamp, 1.0 / 60.0) + self.previousTapAnimationTimestamp = timestamp + + let decelerationRate: CGFloat = 0.98 + let lastTapDeltaTime = max(0.0, timestamp - self.previousTapTimestamp) + let tapAnimationTargetValue: CGFloat + if self.tapCounterView != nil { + tapAnimationTargetValue = 1.0 * CGFloat(pow(Double(decelerationRate), 1200.0 * lastTapDeltaTime)) + } else { + tapAnimationTargetValue = 0.0 + } + + let advancementFraction = deltaTime * UIView.animationDurationFactor() * 120.0 / 60.0 + self.tapAnimationValue = self.tapAnimationValue * (1.0 - advancementFraction) + tapAnimationTargetValue * advancementFraction + + if self.tapAnimationValue <= 0.001 && self.previousTapTimestamp + 2.0 < timestamp { + self.tapAnimationValue = 0.0 + self.tapAnimationLink?.invalidate() + self.tapAnimationLink = nil + + if let tapCounterView = self.tapCounterView { + self.tapCounterView = nil + tapCounterView.alpha = 0.0 + tapCounterView.animateOut(completion: { [weak tapCounterView] in + tapCounterView?.removeFromSuperview() + }) + } + } + + let tapAnimationFactor = max(0.0, min(1.0, self.tapAnimationValue / 0.3)) + + let scaleValue: CGFloat = 1.0 + tapAnimationFactor * 0.5 + self.buttonNode.layer.transform = CATransform3DMakeScale(scaleValue, scaleValue, 1.0) } fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation, arguments: ReactionButtonsAsyncLayoutContainer.Arguments) { self.containerView.frame = CGRect(origin: CGPoint(), size: layout.size) self.containerView.contentView.frame = CGRect(origin: CGPoint(), size: layout.size) self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.size) - animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) + let buttonFrame = CGRect(origin: CGPoint(), size: layout.size) + animation.animator.updatePosition(layer: self.buttonNode.layer, position: buttonFrame.center, completion: nil) + animation.animator.updateBounds(layer: self.buttonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil) - if case .custom(MessageReaction.starsReactionId) = layout.spec.component.reaction.value { + if case .stars = layout.spec.component.reaction.value { let starsEffectLayer: StarsButtonEffectLayer if let current = self.starsEffectLayer { starsEffectLayer = current @@ -1319,7 +1442,7 @@ public final class ReactionButtonsAsyncLayoutContainer { }) if let index = reactions.firstIndex(where: { - if case .custom(MessageReaction.starsReactionId) = $0.reaction.value { + if case .stars = $0.reaction.value { return true } else { return false @@ -1395,3 +1518,83 @@ public final class ReactionButtonsAsyncLayoutContainer { ) } } + +private final class StarsReactionCounterView: UIView { + private let portalSource: PortalSourceView + private let label = ComponentView() + + private var count: Int + + init(count: Int) { + self.count = count + + let portalSource = PortalSourceView() + portalSource.needsGlobalPortal = true + self.portalSource = portalSource + + super.init(frame: CGRect()) + + self.addSubview(portalSource) + + portalSource.frame = CGRect(origin: CGPoint(x: -200.0, y: -200.0), size: CGSize(width: 400.0, height: 400.0)) + + self.update(transition: .immediate) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateIn() { + if let labelView = self.label.view { + labelView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.15) + labelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + + func animateOut(completion: @escaping () -> Void) { + if let labelView = self.label.view { + labelView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.15, removeOnCompletion: false) + labelView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion() + }) + } else { + completion() + } + } + + func add() { + self.count += 1 + self.update(transition: .easeInOut(duration: 0.15)) + } + + func update(transition: ComponentTransition) { + var items: [AnimatedTextComponent.Item] = [] + items.append(AnimatedTextComponent.Item(id: AnyHashable(0), content: .text("+"))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(1), content: .number(self.count, minDigits: 1))) + + let labelSize = self.label.update( + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 40.0, design: .round, weight: .bold), + color: .white, + items: items + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + let labelFrame = CGRect(origin: CGPoint(x: floor((self.portalSource.bounds.width - labelSize.width) * 0.5), y: floor((self.portalSource.bounds.height - labelSize.height) * 0.5)), size: labelSize) + + if let labelView = self.label.view { + if labelView.superview == nil { + self.portalSource.addSubview(labelView) + labelView.layer.shadowColor = UIColor.black.cgColor + labelView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) + labelView.layer.shadowOpacity = 0.45 + labelView.layer.shadowRadius = 9.0 + } + + transition.setFrame(view: labelView, frame: labelFrame) + } + } +} diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 259baf3c123..af576c9cd2a 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -164,6 +164,16 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent strongSelf.file = file strongSelf.updateReactionLayer() }).strict() + case .stars: + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction { + self.file = availableReaction.centerAnimation + self.updateReactionLayer() + break + } + } + } } } else { let iconNode = ASImageNode() @@ -574,6 +584,17 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent strongSelf.updateReactionLayer() strongSelf.updateReactionAccentColor(theme: presentationData.theme) }).strict() + case .stars: + if let availableReactions = self.availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction { + self.file = availableReaction.centerAnimation + self.updateReactionLayer() + self.updateReactionAccentColor(theme: presentationData.theme) + break + } + } + } } } else { self.file = nil @@ -598,6 +619,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent reactionStringValue = value case .custom: reactionStringValue = "" + case .stars: + reactionStringValue = "Star" } } else { reactionStringValue = "" diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index b6e72f5525a..966d2d5a2e5 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1093,7 +1093,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } else if peer.id.isReplies, case .generalSearch = item.peerMode { overrideImage = .repliesIcon } else if peer.id.isAnonymousSavedMessages, case .generalSearch = item.peerMode { - overrideImage = .anonymousSavedMessagesIcon + overrideImage = .anonymousSavedMessagesIcon(isColored: true) } else if peer.isDeleted { overrideImage = .deletedIcon } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index ea7827e4fda..05c6be0606d 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1329,9 +1329,9 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega } } - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) { if let sourceContainer = self.sourceContainer { - sourceContainer.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) + sourceContainer.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion) } } @@ -2604,7 +2604,7 @@ public final class ContextController: ViewController, StandalonePresentableContr } else if let args = self.dismissToReactionOnInputClose { self.dismissToReactionOnInputClose = nil DispatchQueue.main.async { - self.dismissWithReactionImpl(value: args.value, targetView: args.targetView, hideNode: args.hideNode, animateTargetContainer: args.animateTargetContainer, addStandaloneReactionAnimation: args.addStandaloneReactionAnimation, reducedCurve: true, completion: args.completion) + self.dismissWithReactionImpl(value: args.value, targetView: args.targetView, hideNode: args.hideNode, animateTargetContainer: args.animateTargetContainer, addStandaloneReactionAnimation: args.addStandaloneReactionAnimation, reducedCurve: true, onHit: nil, completion: args.completion) } } } @@ -2706,11 +2706,11 @@ public final class ContextController: ViewController, StandalonePresentableContr self.dismissed?() } - public func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: (() -> Void)?) { - self.dismissWithReactionImpl(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: false, completion: completion) + public func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: (() -> Void)?) { + self.dismissWithReactionImpl(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: false, onHit: onHit, completion: completion) } - private func dismissWithReactionImpl(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: (() -> Void)?) { + private func dismissWithReactionImpl(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: (() -> Void)?) { if viewTreeContainsFirstResponder(view: self.view) { self.dismissToReactionOnInputClose = (value, targetView, hideNode, animateTargetContainer, addStandaloneReactionAnimation, completion) self.view.endEditing(true) @@ -2719,7 +2719,7 @@ public final class ContextController: ViewController, StandalonePresentableContr if !self.wasDismissed { self.wasDismissed = true - self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: { [weak self] in + self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 8e840e14f13..3acfebe45ef 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -1547,8 +1547,6 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo additive: true, completion: { [weak self] _ in Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, { - contentNode.containingItem.isExtractedToContextPreview = false - contentNode.containingItem.isExtractedToContextPreviewUpdated?(false) if let strongSelf = self, let contentNode = strongSelf.itemContentNode { switch contentNode.containingItem { @@ -1559,6 +1557,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } + contentNode.containingItem.isExtractedToContextPreview = false + contentNode.containingItem.isExtractedToContextPreviewUpdated?(false) + completion() }) } @@ -1680,7 +1681,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) { guard let reactionContextNode = self.reactionContextNode else { self.requestAnimateOut(.default, completion) return @@ -1709,7 +1710,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo intermediateCompletion() }) - reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, completion: { [weak self] in + reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, onHit: onHit, completion: { [weak self] in guard let strongSelf = self else { return } diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index 5137890b60e..01938595e2b 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -28,7 +28,7 @@ protocol ContextControllerPresentationNode: ASDisplayNode { stateTransition: ContextControllerPresentationNodeStateTransition? ) - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) func cancelReactionAnimation() func highlightGestureMoved(location: CGPoint, hover: Bool) diff --git a/submodules/ContextUI/Sources/ContextSourceContainer.swift b/submodules/ContextUI/Sources/ContextSourceContainer.swift index ee940e2e490..c54999cb685 100644 --- a/submodules/ContextUI/Sources/ContextSourceContainer.swift +++ b/submodules/ContextUI/Sources/ContextSourceContainer.swift @@ -269,8 +269,8 @@ final class ContextSourceContainer: ASDisplayNode { self.presentationNode.cancelReactionAnimation() } - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { - self.presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) { + self.presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion) } func setItems(items: Signal, animated: Bool) { @@ -543,9 +543,9 @@ final class ContextSourceContainer: ASDisplayNode { } } - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) { if let activeSource = self.activeSource { - activeSource.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) + activeSource.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion) } else { completion() } diff --git a/submodules/ContextUI/Sources/ReactionPreviewView.swift b/submodules/ContextUI/Sources/ReactionPreviewView.swift index 753087581c2..1e12087af30 100644 --- a/submodules/ContextUI/Sources/ReactionPreviewView.swift +++ b/submodules/ContextUI/Sources/ReactionPreviewView.swift @@ -36,7 +36,7 @@ final class ReactionPreviewView: UIView { size: size, placeholderColor: .clear, themeColor: .white, - loopMode: .count(0) + loopMode: .forever ), isVisibleForAnimations: true, action: nil diff --git a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift index 7d6291b9aa4..9ae8bce77c3 100644 --- a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift +++ b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift @@ -85,7 +85,7 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } else if chatPeer.id.isReplies { self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .repliesIcon) } else if chatPeer.id.isAnonymousSavedMessages { - self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .anonymousSavedMessagesIcon) + self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .anonymousSavedMessagesIcon(isColored: true)) } else { var overrideImage: AvatarNodeImageOverride? if chatPeer.isDeleted { diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index eea065a5f12..4ec3efdd878 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -538,6 +538,17 @@ public extension ContainedViewLayoutTransition { } } + func animateScaleWithKeyframes(layer: CALayer, keyframes: [CGFloat], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self { + case .immediate: + completion?(true) + case let .animated(duration, curve): + layer.animateKeyframes(values: keyframes.map { NSNumber(value: Float($0)) }, duration: duration, keyPath: "transform.scale", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in + completion?(value) + }) + } + } + func animateFrame(node: ASDisplayNode, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { case .immediate: diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 60c371dd67e..ba10231f7e7 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -745,8 +745,10 @@ open class NavigationController: UINavigationController, ContainableController, let modalContainer = self.modalContainers[i] var isStandaloneModal = false - if let controller = modalContainer.container.controllers.first, case .standaloneModal = controller.navigationPresentation { - isStandaloneModal = true + if let controller = modalContainer.container.controllers.first { + if [.standaloneModal, .standaloneFlatModal].contains(controller.navigationPresentation) { + isStandaloneModal = true + } } let containerTransition: ContainedViewLayoutTransition diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index 61cfc02b45b..a2a123c0730 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -43,6 +43,11 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL requiresModal = true beginsModal = true isStandalone = true + case .standaloneFlatModal: + requiresModal = true + beginsModal = true + isStandalone = true + isFlat = true case .modalInLargeLayout: switch layout.metrics.widthClass { case .compact: diff --git a/submodules/Display/Source/PresentationContext.swift b/submodules/Display/Source/PresentationContext.swift index 804cabd50a1..57b6a5f0912 100644 --- a/submodules/Display/Source/PresentationContext.swift +++ b/submodules/Display/Source/PresentationContext.swift @@ -18,7 +18,7 @@ public enum PresentationContextType { public final class PresentationContext { private var _view: UIView? - var view: UIView? { + public var view: UIView? { get { return self._view } set(value) { @@ -52,7 +52,12 @@ public final class PresentationContext { return self.view != nil && self.layout != nil } - private(set) var controllers: [(ContainableController, PresentationSurfaceLevel)] = [] + public private(set) var controllers: [(ContainableController, PresentationSurfaceLevel)] = [] { + didSet { + self.controllersUpdated(self.controllers) + } + } + public var controllersUpdated: ([(ContainableController, PresentationSurfaceLevel)]) -> Void = { _ in } private var presentationDisposables = DisposableSet() @@ -123,6 +128,9 @@ public final class PresentationContext { return (containerLayout, CGRect(origin: CGPoint(), size: containerLayout.size)) } + public init() { + } + public func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool = false, completion: @escaping () -> Void) { let controllerReady = controller.ready.get() |> filter({ $0 }) diff --git a/submodules/Display/Source/StatusBar.swift b/submodules/Display/Source/StatusBar.swift index e74ef187c32..abd031a5e7c 100644 --- a/submodules/Display/Source/StatusBar.swift +++ b/submodules/Display/Source/StatusBar.swift @@ -81,7 +81,7 @@ public final class StatusBar: ASDisplayNode { } } - var alphaUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var alphaUpdated: ((ContainedViewLayoutTransition) -> Void)? public func updateAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { self.alpha = alpha diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 520910e25e3..a9b46408be4 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -64,6 +64,7 @@ public enum ViewControllerNavigationPresentation { case modal case flatModal case standaloneModal + case standaloneFlatModal case modalInLargeLayout case modalInCompactLayout } diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift index 19eeba7632e..5ef068ce40c 100644 --- a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift @@ -209,7 +209,7 @@ public final class DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() } else { - string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "") + string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: "") } let text = NSMutableAttributedString(string: string) let range = NSMakeRange(0, text.length) diff --git a/submodules/DrawingUI/Sources/DrawingReactionView.swift b/submodules/DrawingUI/Sources/DrawingReactionView.swift index 34148e1fb88..26cae6e22a8 100644 --- a/submodules/DrawingUI/Sources/DrawingReactionView.swift +++ b/submodules/DrawingUI/Sources/DrawingReactionView.swift @@ -264,6 +264,24 @@ public class DrawingReactionEntityView: DrawingStickerEntityView { } }) } + case .stars: + let _ = (self.context.engine.stickers.availableReactions() + |> take(1) + |> deliverOnMainQueue).start(next: { availableReactions in + guard let availableReactions else { + return + } + var animation: TelegramMediaFile? + for reaction in availableReactions.reactions { + if reaction.value == updateReaction.reaction { + animation = reaction.selectAnimation + break + } + } + if let animation { + continueWithAnimationFile(animation) + } + }) } } diff --git a/submodules/GalleryData/Sources/GalleryData.swift b/submodules/GalleryData/Sources/GalleryData.swift index 5e9bfedeccf..236db5f6a38 100644 --- a/submodules/GalleryData/Sources/GalleryData.swift +++ b/submodules/GalleryData/Sources/GalleryData.swift @@ -256,7 +256,13 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati } } - if message.containsSecretMedia { + if let adAttribute = message.adAttribute, adAttribute.hasContentMedia { + let gallery = GalleryController(context: context, source: .standaloneMessage(message, mediaIndex), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: nil, playbackRate: 1.0, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in + navigationController?.replaceTopController(controller, animated: false, ready: ready) + }, baseNavigationController: navigationController, actionInteraction: actionInteraction) + gallery.temporaryDoNotWaitForReady = autoplayingVideo + return .gallery(.single(gallery)) + } else if message.containsSecretMedia { let gallery = SecretMediaPreviewController(context: context, messageId: message.id) return .secretGallery(gallery) } else { diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index a8608f27310..cbe260d608d 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -53,6 +53,10 @@ swift_library( "//submodules/TooltipUI", "//submodules/TelegramNotices", "//submodules/Pasteboard", + "//submodules/AdUI", + "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", + "//submodules/TelegramUI/Components/Ads/AdsReportScreen", + "//submodules/UrlHandling", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index c4bbe079186..c73e666e38c 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -29,6 +29,8 @@ import Pasteboard import Speak import TranslateUI import TelegramNotices +import SolidRoundedButtonNode +import UrlHandling private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white) private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white) @@ -145,6 +147,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll private let textNode: ImmediateTextNodeWithEntities private var spoilerTextNode: ImmediateTextNodeWithEntities? private var dustNode: InvisibleInkDustNode? + private var buttonNode: SolidRoundedButtonNode? + private var buttonIconNode: ASImageNode? private var textSelectionNode: TextSelectionNode? @@ -164,7 +168,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll private var currentMessageText: NSAttributedString? private var currentAuthorNameText: String? private var currentDateText: String? - + private var currentMessage: Message? private var currentWebPageAndMedia: (TelegramMediaWebpage, Media)? private let messageContextDisposable = MetaDisposable() @@ -193,6 +197,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll var performAction: ((GalleryControllerInteractionTapAction) -> Void)? var openActionOptions: ((GalleryControllerInteractionTapAction, Message) -> Void)? + private var isAd: Bool { + if self.currentMessage?.adAttribute != nil { + return true + } + return false + } + var content: ChatItemGalleryFooterContent = .info { didSet { if self.content != oldValue { @@ -208,8 +219,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.currentIsPaused = true self.authorNameNode.isHidden = true self.dateNode.isHidden = true - self.hasSeekControls = seekable - if status == .Local { + self.hasSeekControls = seekable && !self.isAd + + if status == .Local && !self.isAd { self.playbackControlButton.isHidden = false self.playPauseIconNode.enqueueState(.play, animated: true) } else { @@ -236,16 +248,20 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.currentIsPaused = paused self.authorNameNode.isHidden = true self.dateNode.isHidden = true - self.hasSeekControls = seekable - self.playbackControlButton.isHidden = false - let icon: PlayPauseIconNodeState - if let wasPlaying = self.wasPlaying { - icon = wasPlaying ? .pause : .play + if !self.isAd { + self.playbackControlButton.isHidden = false + let icon: PlayPauseIconNodeState + if let wasPlaying = self.wasPlaying { + icon = wasPlaying ? .pause : .play + } else { + icon = paused ? .play : .pause + } + self.playPauseIconNode.enqueueState(icon, animated: true) + self.hasSeekControls = seekable } else { - icon = paused ? .play : .pause + self.hasSeekControls = false } - self.playPauseIconNode.enqueueState(icon, animated: true) self.statusButtonNode.isHidden = true self.statusNode.isHidden = true } @@ -770,9 +786,18 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } - func setup(origin: GalleryItemOriginData?, caption: NSAttributedString) { - let titleText = origin?.title - let dateText = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: $0).string } + func setup(origin: GalleryItemOriginData?, caption: NSAttributedString, isAd: Bool = false) { + var titleText = origin?.title + var dateText = origin?.timestamp.flatMap { humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: $0).string } + + let caption = caption.mutableCopy() as! NSMutableAttributedString + if isAd { + if let titleText, !titleText.isEmpty { + caption.insert(NSAttributedString(string: titleText + "\n", font: Font.semibold(17.0), textColor: .white), at: 0) + } + titleText = nil + dateText = nil + } if self.currentMessageText != caption || self.currentAuthorNameText != titleText || self.currentDateText != dateText { self.currentMessageText = caption @@ -820,12 +845,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if Namespaces.Message.allNonRegular.contains(message.id.namespace) || message.timestamp == 0 { displayInfo = false } - - var canDelete: Bool - var canShare = !message.containsSecretMedia && !Namespaces.Message.allNonRegular.contains(message.id.namespace) - var canFullscreen = false - + var canDelete: Bool + var canShare = !message.containsSecretMedia && !Namespaces.Message.allNonRegular.contains(message.id.namespace) && message.adAttribute == nil + var canEdit = false var isImage = false var isVideo = false @@ -912,6 +935,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll canDelete = false } + if let _ = message.adAttribute { + displayInfo = false + canFullscreen = false + } + var authorNameText: String? if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { authorNameText = authorSignature @@ -922,13 +950,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } var dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: message.timestamp).string - if !displayInfo { - authorNameText = "" - dateText = "" - canEdit = false - } - - var messageText = NSAttributedString(string: "") + + var messageText = NSMutableAttributedString(string: "") var hasCaption = false for media in message.media { if media is TelegramMediaPaidContent { @@ -991,7 +1014,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll codeHighlightState.disposable.dispose() } - messageText = galleryCaptionStringWithAppliedEntities(context: self.context, text: text, entities: entities, message: message, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight) + messageText = galleryCaptionStringWithAppliedEntities(context: self.context, text: text, entities: entities, message: message, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight).mutableCopy() as! NSMutableAttributedString + if let _ = message.adAttribute { + messageText.insert(NSAttributedString(string: (authorNameText ?? "") + "\n", font: Font.semibold(17.0), textColor: .white), at: 0) + } + } + + if !displayInfo { + authorNameText = "" + dateText = "" + canEdit = false } if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canFullscreen != !self.fullscreenButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText { @@ -1028,6 +1060,30 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.actionButton.isHidden = !canShare self.editButton.isHidden = !canEdit + if let adAttribute = message.adAttribute { + if self.buttonNode == nil { + let buttonNode = SolidRoundedButtonNode(title: adAttribute.buttonText, theme: SolidRoundedButtonTheme(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.15), foregroundColor: UIColor(rgb: 0xffffff)), height: 50.0, cornerRadius: 11.0) + buttonNode.pressed = { [weak self] in + guard let self else { + return + } + self.performAction?(.ad(message.id)) + } + self.contentNode.addSubnode(buttonNode) + self.buttonNode = buttonNode + + if !isTelegramMeLink(adAttribute.url) { + let buttonIconNode = ASImageNode() + buttonIconNode.displaysAsynchronously = false + buttonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: .white) + buttonNode.addSubnode(buttonIconNode) + self.buttonIconNode = buttonIconNode + } + } + } else if let buttonNode = self.buttonNode { + buttonNode.removeFromSupernode() + } + self.requestLayout?(.immediate) } } @@ -1176,6 +1232,21 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.scrollWrapperNode.layer.mask?.frame = self.scrollWrapperNode.bounds self.scrollWrapperNode.layer.mask?.removeAllAnimations() } + + if let buttonNode = self.buttonNode { + let buttonHeight = buttonNode.updateLayout(width: constrainSize.width, transition: transition) + transition.updateFrame(node: buttonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: scrollWrapperNodeFrame.maxY + 8.0), size: CGSize(width: constrainSize.width, height: buttonHeight))) + + if let buttonIconNode = self.buttonIconNode, let icon = buttonIconNode.image { + transition.updateFrame(node: buttonIconNode, frame: CGRect(origin: CGPoint(x: constrainSize.width - icon.size.width - 9.0, y: 9.0), size: icon.size)) + } + + if let _ = self.scrubberView { + panelHeight += 68.0 + } else { + panelHeight += 22.0 + } + } } textFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + textOffset), size: textSize) @@ -1211,6 +1282,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } } + if let _ = self.buttonNode { + panelHeight -= 44.0 + } + let scrubberFrame = CGRect(origin: CGPoint(x: leftInset, y: scrubberY), size: CGSize(width: width - leftInset - rightInset, height: 34.0)) scrubberView.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size)) @@ -1309,6 +1384,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.forwardButton.alpha = self.hasSeekControls ? 1.0 : 0.0 self.statusNode.alpha = 1.0 self.playbackControlButton.alpha = 1.0 + self.buttonNode?.alpha = 1.0 self.scrollWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } @@ -1333,6 +1409,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll self.forwardButton.alpha = 0.0 self.statusNode.alpha = 0.0 self.playbackControlButton.alpha = 0.0 + self.buttonNode?.alpha = 0.0 self.scrollWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { _ in completion() }) diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index ff88e9092ec..ae0d7a50091 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -490,6 +490,7 @@ public enum GalleryControllerInteractionTapAction { case botCommand(String) case hashtag(String?, String) case timecode(Double, String) + case ad(MessageId) } public enum GalleryControllerItemNodeAction { @@ -968,6 +969,8 @@ public class GalleryController: ViewController, StandalonePresentableController, strongSelf.actionInteraction?.openHashtag(peerName, hashtag) case let .timecode(timecode, _): strongSelf.galleryNode.pager.centralItemNode()?.processAction(.timecode(timecode)) + case let .ad(messageId): + strongSelf.actionInteraction?.openAd(messageId) } } } @@ -1223,6 +1226,8 @@ public class GalleryController: ViewController, StandalonePresentableController, ]) ]) strongSelf.present(actionSheet, in: .window(.root)) + case .ad: + break } } } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index edfca6bc771..1a39e933c7e 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -21,6 +21,10 @@ import ShareController import UndoUI import ContextUI import SaveToCameraRoll +import Pasteboard +import AdUI +import AdsInfoScreen +import AdsReportScreen enum ChatMediaGalleryThumbnail: Equatable { case image(ImageMediaReference) @@ -168,7 +172,9 @@ class ChatImageGalleryItem: GalleryItem { } } - if let location = self.location { + if let _ = message.adAttribute { + node._title.set(.single(self.presentationData.strings.Gallery_Ad)) + } else if let location = self.location { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string)) } @@ -530,6 +536,132 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.play() self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) } + + private func adMenuMainItems() -> Signal<[ContextMenuItem], NoError> { + guard let message = self.message, let adAttribute = message.adAttribute else { + return .single([]) + } + + let context = self.context + let presentationData = self.presentationData + var actions: [ContextMenuItem] = [] + if adAttribute.canReport { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AboutAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.dismissWithoutContent) + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(AdsInfoScreen(context: context)) + } + }))) + + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.default) + + let _ = (context.engine.messages.reportAdMessage(peerId: message.id.peerId, opaqueId: adAttribute.opaqueId, option: nil) + |> deliverOnMainQueue).start(next: { [weak self] result in + if case let .options(title, options) = result { + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController( + AdsReportScreen( + context: context, + peerId: message.id.peerId, + opaqueId: adAttribute.opaqueId, + title: title, + options: options, + forceDark: true, + completed: { + if let navigationController = self?.baseNavigationController() as? NavigationController, let chatController = navigationController.viewControllers.last as? ChatController { + chatController.removeAd(opaqueId: adAttribute.opaqueId) + } + } + ) + ) + } + } + }) + }))) + + actions.append(.separator) + + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_RemoveAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(controller) + } + }) + }))) + } else { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.dismissWithoutContent) + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(AdInfoScreen(context: context, forceDark: true)) + } + }))) + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + if !context.isPremium && !premiumConfiguration.isPremiumDisabled { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(controller) + } + }) + }))) + } + + if !message.text.isEmpty { + actions.append(.separator) + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + var messageEntities: [MessageTextEntity]? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + } + } + + storeMessageTextInPasteboard(message.text, entities: messageEntities) + + Queue.mainQueue().after(0.2, { + guard let self, let controller = self.galleryController() else { + return + } + controller.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_MessageCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + + f(.default) + }))) + } + } + + return .single(actions) + } private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { let peer: Signal @@ -607,11 +739,15 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { - let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() + let items: Signal<[ContextMenuItem], NoError> + if let message = self.message, let _ = message.adAttribute { + items = self.adMenuMainItems() + } else { + items = self.contextMenuMainItems() + } guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) controller.presentInGlobalOverlay(contextController) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 7c82a528e0c..074f443cdaa 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -23,6 +23,10 @@ import OpenInExternalAppUI import AVKit import TextFormat import SliderContextItem +import Pasteboard +import AdUI +import AdsInfoScreen +import AdsReportScreen public enum UniversalVideoGalleryItemContentInfo { case message(Message, Int?) @@ -89,6 +93,8 @@ public class UniversalVideoGalleryItem: GalleryItem { if let indexData = self.indexData { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string)) + } else if case let .message(message, _) = self.contentInfo, let _ = message.adAttribute { + node._title.set(.single(self.presentationData.strings.Gallery_Ad)) } node.setupItem(self) @@ -1474,7 +1480,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let _ = message.paidContent, message.id.namespace == Namespaces.Message.Local { hasMoreButton = false } - + + if let _ = message.adAttribute { + hasMoreButton = true + } + if hasMoreButton { let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! moreMenuItem.accessibilityLabel = self.presentationData.strings.Common_More @@ -1528,15 +1538,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.playbackRatePromise.set(self.playbackRate ?? 1.0) + var isAd = false if let contentInfo = item.contentInfo { switch contentInfo { case let .message(message, _): + isAd = message.adAttribute != nil self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected) case let .webPage(webPage, media, _): self.footerContentNode.setWebPage(webPage, media: media) } } - self.footerContentNode.setup(origin: item.originData, caption: item.caption) + self.footerContentNode.setup(origin: item.originData, caption: item.caption, isAd: isAd) } override func controlsVisibilityUpdated(isVisible: Bool) { @@ -2478,9 +2490,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } var dismissImpl: (() -> Void)? - let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems(dismiss: { - dismissImpl?() - }) + let items: Signal<[ContextMenuItem], NoError> + if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { + items = self.adMenuMainItems() + } else { + items = self.contextMenuMainItems(dismiss: { + dismissImpl?() + }) + } + let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) self.isShowingContextMenuPromise.set(true) controller.presentInGlobalOverlay(contextController) @@ -2504,6 +2522,133 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return speedList } + + private func adMenuMainItems() -> Signal<[ContextMenuItem], NoError> { + guard case let .message(message, _) = self.item?.contentInfo, let adAttribute = message.adAttribute else { + return .single([]) + } + + let context = self.context + let presentationData = self.presentationData + var actions: [ContextMenuItem] = [] + if adAttribute.canReport { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AboutAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.dismissWithoutContent) + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(AdsInfoScreen(context: context, forceDark: true)) + } + }))) + + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.default) + + let _ = (context.engine.messages.reportAdMessage(peerId: message.id.peerId, opaqueId: adAttribute.opaqueId, option: nil) + |> deliverOnMainQueue).start(next: { [weak self] result in + if case let .options(title, options) = result { + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController( + AdsReportScreen( + context: context, + peerId: message.id.peerId, + opaqueId: adAttribute.opaqueId, + title: title, + options: options, + forceDark: true, + completed: { + if let navigationController = self?.baseNavigationController() as? NavigationController, let chatController = navigationController.viewControllers.last as? ChatController { + chatController.removeAd(opaqueId: adAttribute.opaqueId) + } + } + ) + ) + } + } + }) + }))) + + actions.append(.separator) + + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_RemoveAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(controller) + } + }) + }))) + } else { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Info, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.dismissWithoutContent) + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(AdInfoScreen(context: context, forceDark: true)) + } + }))) + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + if !context.isPremium && !premiumConfiguration.isPremiumDisabled { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: true, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: true, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + if let navigationController = self?.baseNavigationController() as? NavigationController { + navigationController.pushViewController(controller) + } + }) + }))) + } + + if !message.text.isEmpty { + actions.append(.separator) + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + var messageEntities: [MessageTextEntity]? + for attribute in message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + messageEntities = attribute.entities + } + } + + storeMessageTextInPasteboard(message.text, entities: messageEntities) + + Queue.mainQueue().after(0.2, { + guard let self, let controller = self.galleryController() else { + return + } + controller.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_MessageCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + + f(.default) + }))) + } + } + + return .single(actions) + } + private func contextMenuMainItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { guard let videoNode = self.videoNode, let item = self.item else { diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index 5e27921837f..1ff599254f8 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -105,6 +105,7 @@ public class MediaDustNode: ASDisplayNode { private var staticNode: ASImageNode? private var staticParams: CGSize? + public var revealOnTap = true public var isRevealed = false private var isExploding = false @@ -218,6 +219,10 @@ public class MediaDustNode: ASDisplayNode { self.tapped() + guard self.revealOnTap else { + return + } + self.isRevealed = true if self.enableAnimations { diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index 2b8ae6f6a49..07dd1849577 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -59,6 +59,7 @@ swift_library( "//submodules/QrCodeUI:QrCodeUI", "//submodules/PromptUI", "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 951a5e4b4ab..3d6ce5ecdc7 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -17,17 +17,30 @@ import ContextUI import TelegramStringFormatting import UndoUI import ItemListDatePickerItem +import TextFormat private final class InviteLinkEditControllerArguments { let context: AccountContext let updateState: ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void + let focusOnItem: (InviteLinksEditEntryTag) -> Void + let errorWithItem: (InviteLinksEditEntryTag) -> Void let scrollToUsage: () -> Void let dismissInput: () -> Void let revoke: () -> Void - init(context: AccountContext, updateState: @escaping ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void, scrollToUsage: @escaping () -> Void, dismissInput: @escaping () -> Void, revoke: @escaping () -> Void) { + init( + context: AccountContext, + updateState: @escaping ((InviteLinkEditControllerState) -> InviteLinkEditControllerState) -> Void, + focusOnItem: @escaping (InviteLinksEditEntryTag) -> Void, + errorWithItem: @escaping (InviteLinksEditEntryTag) -> Void, + scrollToUsage: @escaping () -> Void, + dismissInput: @escaping () -> Void, + revoke: @escaping () -> Void) + { self.context = context self.updateState = updateState + self.focusOnItem = focusOnItem + self.errorWithItem = errorWithItem self.scrollToUsage = scrollToUsage self.dismissInput = dismissInput self.revoke = revoke @@ -36,6 +49,7 @@ private final class InviteLinkEditControllerArguments { private enum InviteLinksEditSection: Int32 { case title + case subscriptionFee case requestApproval case time case usage @@ -43,6 +57,7 @@ private enum InviteLinksEditSection: Int32 { } private enum InviteLinksEditEntryTag: ItemListItemTag { + case subscriptionFee case usage func isEqual(to other: ItemListItemTag) -> Bool { @@ -75,18 +90,23 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case title(PresentationTheme, String, String) case titleInfo(PresentationTheme, String) - case requestApproval(PresentationTheme, String, Bool) + + case subscriptionFeeToggle(PresentationTheme, String, Bool, Bool) + case subscriptionFee(PresentationTheme, String, Bool, Int64?, String, Int64?) + case subscriptionFeeInfo(PresentationTheme, String) + + case requestApproval(PresentationTheme, String, Bool, Bool) case requestApprovalInfo(PresentationTheme, String) case timeHeader(PresentationTheme, String) - case timePicker(PresentationTheme, InviteLinkTimeLimit) - case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool) - case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool) + case timePicker(PresentationTheme, InviteLinkTimeLimit, Bool) + case timeExpiryDate(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool) + case timeCustomPicker(PresentationTheme, PresentationDateTimeFormat, Int32?, Bool, Bool, Bool) case timeInfo(PresentationTheme, String) case usageHeader(PresentationTheme, String) - case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit) - case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool) + case usagePicker(PresentationTheme, PresentationDateTimeFormat, InviteLinkUsageLimit, Bool) + case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool, Bool) case usageInfo(PresentationTheme, String) case revoke(PresentationTheme, String) @@ -95,6 +115,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { switch self { case .titleHeader, .title, .titleInfo: return InviteLinksEditSection.title.rawValue + case .subscriptionFeeToggle, .subscriptionFee, .subscriptionFeeInfo: + return InviteLinksEditSection.subscriptionFee.rawValue case .requestApproval, .requestApprovalInfo: return InviteLinksEditSection.requestApproval.rawValue case .timeHeader, .timePicker, .timeExpiryDate, .timeCustomPicker, .timeInfo: @@ -114,30 +136,36 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return 1 case .titleInfo: return 2 - case .requestApproval: + case .subscriptionFeeToggle: return 3 - case .requestApprovalInfo: + case .subscriptionFee: return 4 - case .timeHeader: + case .subscriptionFeeInfo: return 5 - case .timePicker: + case .requestApproval: return 6 - case .timeExpiryDate: + case .requestApprovalInfo: return 7 - case .timeCustomPicker: + case .timeHeader: return 8 - case .timeInfo: + case .timePicker: return 9 - case .usageHeader: + case .timeExpiryDate: return 10 - case .usagePicker: + case .timeCustomPicker: return 11 - case .usageCustomPicker: + case .timeInfo: return 12 - case .usageInfo: + case .usageHeader: return 13 - case .revoke: + case .usagePicker: return 14 + case .usageCustomPicker: + return 15 + case .usageInfo: + return 16 + case .revoke: + return 17 } } @@ -161,8 +189,26 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .requestApproval(lhsTheme, lhsText, lhsValue): - if case let .requestApproval(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .subscriptionFeeToggle(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .subscriptionFeeToggle(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .subscriptionFee(lhsTheme, lhsText, lhsValue, lhsEnabled, lhsLabel, lhsMaxValue): + if case let .subscriptionFee(rhsTheme, rhsText, rhsValue, rhsEnabled, rhsLabel, rhsMaxValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled, lhsLabel == rhsLabel, lhsMaxValue == rhsMaxValue { + return true + } else { + return false + } + case let .subscriptionFeeInfo(lhsTheme, lhsText): + if case let .subscriptionFeeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .requestApproval(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .requestApproval(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false @@ -179,20 +225,20 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .timePicker(lhsTheme, lhsValue): - if case let .timePicker(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue { + case let .timePicker(lhsTheme, lhsValue, lhsEnabled): + if case let .timePicker(rhsTheme, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false } - case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive): - if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive { + case let .timeExpiryDate(lhsTheme, lhsDateTimeFormat, lhsDate, lhsActive, lhsEnabled): + if case let .timeExpiryDate(rhsTheme, rhsDateTimeFormat, rhsDate, rhsActive, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsActive == rhsActive, lhsEnabled == rhsEnabled { return true } else { return false } - case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection): - if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsDisplayingDateSelection, rhsDisplayingTimeSelection) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsDisplayingDateSelection == rhsDisplayingDateSelection, lhsDisplayingTimeSelection == rhsDisplayingTimeSelection { + case let .timeCustomPicker(lhsTheme, lhsDateTimeFormat, lhsDate, lhsDisplayingDateSelection, lhsDisplayingTimeSelection, lhsEnabled): + if case let .timeCustomPicker(rhsTheme, rhsDateTimeFormat, rhsDate, rhsDisplayingDateSelection, rhsDisplayingTimeSelection, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsDate == rhsDate, lhsDisplayingDateSelection == rhsDisplayingDateSelection, lhsDisplayingTimeSelection == rhsDisplayingTimeSelection, lhsEnabled == rhsEnabled { return true } else { return false @@ -209,14 +255,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } else { return false } - case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue): - if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue { + case let .usagePicker(lhsTheme, lhsDateTimeFormat, lhsValue, lhsEnabled): + if case let .usagePicker(rhsTheme, rhsDateTimeFormat, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsValue == rhsValue, lhsEnabled == rhsEnabled { return true } else { return false } - case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue): - if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue { + case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue, lhsEnabled): + if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue, lhsEnabled == rhsEnabled { return true } else { return false @@ -246,7 +292,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case let .titleHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .title(_, placeholder, value): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, maxLength: 32, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, maxLength: 32, sectionId: self.section, textUpdated: { value in arguments.updateState { state in var updatedState = state updatedState.title = value @@ -255,8 +301,49 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { }, action: {}) case let .titleInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .requestApproval(_, text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .subscriptionFeeToggle(_, text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateState { state in + var updatedState = state + updatedState.subscriptionEnabled = value + if value { + updatedState.requestApproval = false + } else { + updatedState.subscriptionFee = nil + } + return updatedState + } + if value { + Queue.mainQueue().after(0.1) { + arguments.focusOnItem(.subscriptionFee) + } + } + }) + case let .subscriptionFee(_, placeholder, enabled, value, label, maxValue): + let title = NSMutableAttributedString(string: "⭐️", font: Font.semibold(18.0), textColor: .white) + if let range = title.string.range(of: "⭐️") { + title.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: title.string)) + title.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: title.string)) + } + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: title, text: value.flatMap { "\($0)" } ?? "", placeholder: placeholder, label: label, type: .number, spacing: 3.0, enabled: enabled, tag: InviteLinksEditEntryTag.subscriptionFee, sectionId: self.section, textUpdated: { text in + arguments.updateState { state in + var updatedState = state + if var value = Int64(text) { + if let maxValue, value > maxValue { + value = maxValue + arguments.errorWithItem(.subscriptionFee) + } + updatedState.subscriptionFee = value + } else { + updatedState.subscriptionFee = nil + } + return updatedState + } + }, action: {}) + case let .subscriptionFeeInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case let .requestApproval(_, text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.updateState { state in var updatedState = state updatedState.requestApproval = value @@ -267,8 +354,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .timeHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .timePicker(_, value): - return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: true, sectionId: self.section, updated: { value in + case let .timePicker(_, value, enabled): + return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.updateState({ state in var updatedState = state if value != updatedState.time { @@ -279,14 +366,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState }) }) - case let .timeExpiryDate(theme, dateTimeFormat, value, active): + case let .timeExpiryDate(theme, dateTimeFormat, value, active, enabled): let text: String if let value = value { text = stringForMediumDate(timestamp: value, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) } else { text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever } - return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.InviteLink_Create_TimeLimitExpiryDate, enabled: enabled, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { arguments.dismissInput() arguments.updateState { state in var updatedState = state @@ -298,7 +385,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState } }) - case let .timeCustomPicker(_, dateTimeFormat, date, displayingDateSelection, displayingTimeSelection): + case let .timeCustomPicker(_, dateTimeFormat, date, displayingDateSelection, displayingTimeSelection, enabled): + let _ = enabled let title = presentationData.strings.InviteLink_Create_TimeLimitExpiryTime return ItemListDatePickerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, date: date, title: title, displayingDateSelection: displayingDateSelection, displayingTimeSelection: displayingTimeSelection, sectionId: self.section, style: .blocks, toggleDateSelection: { arguments.updateState({ state in @@ -329,8 +417,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .usageHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .usagePicker(_, dateTimeFormat, value): - return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: true, sectionId: self.section, updated: { value in + case let .usagePicker(_, dateTimeFormat, value, enabled): + return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.dismissInput() arguments.updateState({ state in var updatedState = state @@ -342,14 +430,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { return updatedState }) }) - case let .usageCustomPicker(theme, value, focused, customValue): + case let .usageCustomPicker(theme, value, focused, customValue, enabled): let text: String if let value = value, value != 0 { text = String(value) } else { text = focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited } - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, secondaryStyle: !customValue, tag: InviteLinksEditEntryTag.usage, sectionId: self.section, textUpdated: { updatedText in + return ItemListSingleLineInputItem(context: arguments.context, presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, enabled: enabled, selectAllOnFocus: true, secondaryStyle: !customValue, tag: InviteLinksEditEntryTag.usage, sectionId: self.section, textUpdated: { updatedText in arguments.updateState { state in var updatedState = state if updatedText.isEmpty { @@ -391,26 +479,54 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } } -private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData) -> [InviteLinksEditEntry] { +private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData, configuration: StarsSubscriptionConfiguration) -> [InviteLinksEditEntry] { var entries: [InviteLinksEditEntry] = [] entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased())) entries.append(.title(presentationData.theme, presentationData.strings.InviteLink_Create_LinkName, state.title)) entries.append(.titleInfo(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameInfo)) + let isEditingEnabled = invite?.pricing == nil + let isSubscription = state.subscriptionEnabled + if !isGroup { + entries.append(.subscriptionFeeToggle(presentationData.theme, presentationData.strings.InviteLink_Create_Fee, state.subscriptionEnabled, isEditingEnabled)) + if state.subscriptionEnabled { + var label: String = "" + if let subscriptionFee = state.subscriptionFee, subscriptionFee > 0 { + var usdRate = 0.012 + if let usdWithdrawRate = configuration.usdWithdrawRate { + usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 + } + label = presentationData.strings.InviteLink_Create_FeePerMonth("≈\(formatTonUsdValue(subscriptionFee, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string + } + entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, configuration.maxFee)) + } + let infoText: String + if let _ = invite, state.subscriptionEnabled { + infoText = presentationData.strings.InviteLink_Create_FeeEditInfo + } else { + infoText = presentationData.strings.InviteLink_Create_FeeInfo + } + entries.append(.subscriptionFeeInfo(presentationData.theme, infoText)) + } + if !isPublic { - entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval)) + entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval, isEditingEnabled && !isSubscription)) var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel - if state.requestApproval { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel + if isSubscription { + requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalFeeUnavailable } else { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + if state.requestApproval { + requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel + } else { + requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + } } entries.append(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText)) } entries.append(.timeHeader(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimit.uppercased())) - entries.append(.timePicker(presentationData.theme, state.time)) + entries.append(.timePicker(presentationData.theme, state.time, isEditingEnabled)) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) var time: Int32? @@ -419,21 +535,21 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: } else if let value = state.time.value { time = currentTime + value } - entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate || state.pickingExpiryTime)) + entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate || state.pickingExpiryTime, isEditingEnabled)) if state.pickingExpiryDate || state.pickingExpiryTime { - entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate, state.pickingExpiryTime)) + entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, time, state.pickingExpiryDate, state.pickingExpiryTime, isEditingEnabled)) } entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo)) if !state.requestApproval || isPublic { entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) - entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage)) + entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage, isEditingEnabled)) var customValue = false if case .custom = state.usage { customValue = true } - entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue)) + entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue, isEditingEnabled)) entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo)) } @@ -449,6 +565,8 @@ private struct InviteLinkEditControllerState: Equatable { var usage: InviteLinkUsageLimit var time: InviteLinkTimeLimit var requestApproval = false + var subscriptionEnabled = false + var subscriptionFee: Int64? var pickingExpiryDate = false var pickingExpiryTime = false var pickingUsageLimit = false @@ -460,7 +578,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let actionsDisposable = DisposableSet() let initialState: InviteLinkEditControllerState - if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _) = invite { + if let invite = invite, case let .link(_, title, _, requestApproval, _, _, _, _, expireDate, usageLimit, count, _, pricing) = invite { var usageLimit = usageLimit if let limit = usageLimit, let count = count, count > 0 { usageLimit = limit - count @@ -478,9 +596,9 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio timeLimit = .unlimited } - initialState = InviteLinkEditControllerState(title: title ?? "", usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: requestApproval, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(title: title ?? "", usage: InviteLinkUsageLimit(value: usageLimit), time: timeLimit, requestApproval: requestApproval, subscriptionEnabled: pricing != nil, subscriptionFee: pricing?.amount, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) } else { - initialState = InviteLinkEditControllerState(title: "", usage: .unlimited, time: .unlimited, requestApproval: false, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) + initialState = InviteLinkEditControllerState(title: "", usage: .unlimited, time: .unlimited, requestApproval: false, subscriptionEnabled: false, subscriptionFee: nil, pickingExpiryDate: false, pickingExpiryTime: false, pickingUsageLimit: false) } let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -492,9 +610,15 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? var scrollToUsageImpl: (() -> Void)? + var focusImpl: ((InviteLinksEditEntryTag) -> Void)? + var errorImpl: ((InviteLinksEditEntryTag) -> Void)? let arguments = InviteLinkEditControllerArguments(context: context, updateState: { f in updateState(f) + }, focusOnItem: { tag in + focusImpl?(tag) + }, errorWithItem: { tag in + errorImpl?(tag) }, scrollToUsage: { scrollToUsageImpl?() }, dismissInput: { @@ -555,6 +679,8 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let configuration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let previousState = Atomic(value: nil) let signal = combineLatest( presentationData, @@ -570,14 +696,21 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio dismissImpl?() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: true, action: { + var doneIsEnabled = true + if state.subscriptionEnabled { + if (state.subscriptionFee ?? 0) == 0 { + doneIsEnabled = false + } + } + + let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: doneIsEnabled, action: { updateState { state in var updatedState = state updatedState.updating = true return updatedState } - let expireDate: Int32? + var expireDate: Int32? if case let .custom(value) = state.time { expireDate = value } else if let value = state.time.value { @@ -589,11 +722,20 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let titleString = state.title.trimmingCharacters(in: .whitespacesAndNewlines) let title = titleString.isEmpty ? nil : titleString - let usageLimit = state.usage.value - let requestNeeded = state.requestApproval && !isPublic + var usageLimit = state.usage.value + var requestNeeded: Bool? = state.requestApproval && !isPublic if invite == nil { - let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded) + let subscriptionPricing: StarsSubscriptionPricing? + if let subscriptionFee = state.subscriptionFee { + subscriptionPricing = StarsSubscriptionPricing( + period: context.account.testingEnvironment ? StarsSubscriptionPricing.testPeriod : StarsSubscriptionPricing.monthPeriod, + amount: subscriptionFee + ) + } else { + subscriptionPricing = nil + } + let _ = (context.engine.peers.createPeerExportedInvitation(peerId: peerId, title: title, expireDate: expireDate, usageLimit: requestNeeded == true ? 0 : usageLimit, requestNeeded: requestNeeded, subscriptionPricing: subscriptionPricing) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -606,13 +748,24 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }) - } else if let initialInvite = invite, case let .link(link, _, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _) = initialInvite { - if initialExpireDate == expireDate && initialUsageLimit == usageLimit && initialRequestApproval == requestNeeded { + } else if let initialInvite = invite, case let .link(link, initialTitle, _, initialRequestApproval, _, _, _, _, initialExpireDate, initialUsageLimit, _, _, _) = initialInvite { + if (initialExpireDate ?? 0) == expireDate && (initialUsageLimit ?? 0) == usageLimit && initialRequestApproval == requestNeeded && (initialTitle ?? "") == title { completion?(initialInvite) dismissImpl?() return } - let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: link, title: title, expireDate: expireDate, usageLimit: requestNeeded ? 0 : usageLimit, requestNeeded: requestNeeded) + + if (initialExpireDate ?? 0) == expireDate { + expireDate = nil + } + if (initialUsageLimit ?? 0) == usageLimit { + usageLimit = nil + } + if initialRequestApproval == requestNeeded { + requestNeeded = nil + } + + let _ = (context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: link, title: title, expireDate: expireDate, usageLimit: requestNeeded == true ? 0 : usageLimit, requestNeeded: requestNeeded) |> timeout(10, queue: Queue.mainQueue(), alternate: .fail(.generic)) |> deliverOnMainQueue).start(next: { invite in completion?(invite) @@ -630,7 +783,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let previousState = previousState.swap(state) var animateChanges = false - if let previousState = previousState, previousState.pickingExpiryDate != state.pickingExpiryDate || previousState.pickingExpiryTime != state.pickingExpiryTime || previousState.requestApproval != state.requestApproval { + if let previousState = previousState, previousState.pickingExpiryDate != state.pickingExpiryDate || previousState.pickingExpiryTime != state.pickingExpiryTime || previousState.requestApproval != state.requestApproval || previousState.subscriptionEnabled != state.subscriptionEnabled { animateChanges = true } @@ -642,7 +795,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData, configuration: configuration), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } @@ -686,5 +839,43 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio dismissImpl = { [weak controller] in controller?.dismiss() } + focusImpl = { [weak controller] targetTag in + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: targetTag) { + itemNode.focus() + } + } + } + let hapticFeedback = HapticFeedback() + errorImpl = { [weak controller] targetTag in + hapticFeedback.error() + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: targetTag) { + itemNode.animateError() + } + } + } return controller } + +struct StarsSubscriptionConfiguration { + static var defaultValue: StarsSubscriptionConfiguration { + return StarsSubscriptionConfiguration(maxFee: 2500, usdWithdrawRate: 1200) + } + + let maxFee: Int64? + let usdWithdrawRate: Int64? + + fileprivate init(maxFee: Int64?, usdWithdrawRate: Int64?) { + self.maxFee = maxFee + self.usdWithdrawRate = usdWithdrawRate + } + + public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration { + if let data = appConfiguration.data, let value = data["stars_subscription_amount_max"] as? Double, let usdRate = data["stars_usd_withdraw_rate_x1000"] as? Double { + return StarsSubscriptionConfiguration(maxFee: Int64(value), usdWithdrawRate: Int64(usdRate)) + } else { + return .defaultValue + } + } +} diff --git a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift index c37972d0afc..ff177a97be1 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkListController.swift @@ -215,7 +215,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { case let .mainLinkHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .mainLink(_, invite, peers, importersCount, isPublic): - return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, displayImporters: !isPublic, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { + return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, separateButtons: true, displayImporters: !isPublic, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { if let invite = invite { arguments.copyLink(invite) } @@ -239,7 +239,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { arguments.createLink() }) case let .link(_, _, invite, canEdit, _): - return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in + return ItemListInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in arguments.openLink(invite) } contextAction: { invite, node, gesture in arguments.linkContextAction(invite, canEdit, node, gesture) @@ -253,7 +253,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry { arguments.deleteAllRevokedLinks() }) case let .revokedLink(_, _, invite): - return ItemListInviteLinkItem(presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in + return ItemListInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, share: false, sectionId: self.section, style: .blocks) { invite in arguments.openLink(invite) } contextAction: { invite, node, gesture in arguments.linkContextAction(invite, false, node, gesture) @@ -284,7 +284,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, let mainInvite: ExportedInvitation? var isPublic = false if let peer = peer, let address = peer.addressName, !address.isEmpty && admin == nil { - mainInvite = .link(link: "t.me/\(address)", title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil) + mainInvite = .link(link: "t.me/\(address)", title: nil, isPermanent: true, requestApproval: false, isRevoked: false, adminId: EnginePeer.Id(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil) isPublic = true } else if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) { mainInvite = invite @@ -299,7 +299,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, let importersCount: Int32 if let count = importers?.count { importersCount = count - } else if let mainInvite = mainInvite, case let .link(_, _, _, _, _, _, _, _, _, _, count, _) = mainInvite, let count = count { + } else if let mainInvite = mainInvite, case let .link(_, _, _, _, _, _, _, _, _, _, count, _, _) = mainInvite, let count = count { importersCount = count } else { importersCount = 0 @@ -338,7 +338,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, if let additionalInvites = additionalInvites { var index: Int32 = 0 for invite in additionalInvites { - if case let .link(_, _, _, _, _, _, _, _, expireDate, _, _, _) = invite { + if case let .link(_, _, _, _, _, _, _, _, expireDate, _, _, _, _) = invite { entries.append(.link(index, presentationData.theme, invite, canEditLinks, expireDate != nil ? tick : nil)) index += 1 } @@ -351,7 +351,7 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData, } } if admin == nil { - entries.append(.linksInfo(presentationData.theme, presentationData.strings.InviteLink_CreateInfo)) + entries.append(.linksInfo(presentationData.theme, presentationData.strings.InviteLink_CreateNewInfo)) } if let revokedInvites = revokedInvites { @@ -398,7 +398,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var navigationController: (() -> NavigationController?)? - + var dismissTooltipsImpl: (() -> Void)? let actionsDisposable = DisposableSet() @@ -408,7 +408,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio let updateState: ((InviteLinkListControllerState) -> InviteLinkListControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - + let revokeLinkDisposable = MetaDisposable() actionsDisposable.add(revokeLinkDisposable) @@ -533,7 +533,7 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }) }))) - if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 { + if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 { items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { _, f in diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index d69e30ea2be..442350e6419 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -20,18 +20,52 @@ import PresentationDataUtils import DirectionalPanGesture import UndoUI import QrCodeUI +import TextFormat + +private var subscriptionLinkIcon: UIImage? = { + return generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let pathBounds = CGRect(origin: .zero, size: CGSize(width: 40.0, height: 40.0)) + context.addPath(CGPath(ellipseIn: pathBounds, transform: nil)) + context.clip() + + var locations: [CGFloat] = [1.0, 0.0] + let colors: [CGColor] = [UIColor(rgb: 0x87d93b).cgColor, UIColor(rgb: 0x31b73b).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Item List/SubscriptionLink"), color: .white), let cgImage = image.cgImage { + context.draw(cgImage, in: pathBounds) + } + }) +}() class InviteLinkViewInteraction { let context: AccountContext let openPeer: (EnginePeer.Id) -> Void + let openSubscription: (StarsSubscriptionPricing, PeerInvitationImportersState.Importer) -> Void let copyLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void let editLink: (ExportedInvitation) -> Void let contextAction: (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void - init(context: AccountContext, openPeer: @escaping (EnginePeer.Id) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, editLink: @escaping (ExportedInvitation) -> Void, contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void) { + init( + context: AccountContext, + openPeer: @escaping (EnginePeer.Id) -> Void, + openSubscription: @escaping (StarsSubscriptionPricing, PeerInvitationImportersState.Importer) -> Void, + copyLink: @escaping (ExportedInvitation) -> Void, + shareLink: @escaping (ExportedInvitation) -> Void, + editLink: @escaping (ExportedInvitation) -> Void, + contextAction: @escaping (ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void + ) { self.context = context self.openPeer = openPeer + self.openSubscription = openSubscription self.copyLink = copyLink self.shareLink = shareLink self.editLink = editLink @@ -50,6 +84,8 @@ private struct InviteLinkViewTransaction { private enum InviteLinkViewEntryId: Hashable { case link + case subscriptionHeader + case subscriptionPricing case creatorHeader case creator case requestHeader @@ -60,17 +96,23 @@ private enum InviteLinkViewEntryId: Hashable { private enum InviteLinkViewEntry: Comparable, Identifiable { case link(PresentationTheme, ExportedInvitation) + case subscriptionHeader(PresentationTheme, String) + case subscriptionPricing(PresentationTheme, String, String) case creatorHeader(PresentationTheme, String) case creator(PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32) case requestHeader(PresentationTheme, String, String, Bool) case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool) case importerHeader(PresentationTheme, String, String, Bool) - case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool, Bool) + case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool, Bool, PeerInvitationImportersState.Importer?, StarsSubscriptionPricing?) var stableId: InviteLinkViewEntryId { switch self { case .link: return .link + case .subscriptionHeader: + return .subscriptionHeader + case .subscriptionPricing: + return .subscriptionPricing case .creatorHeader: return .creatorHeader case .creator: @@ -81,7 +123,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { return .request(peer.id) case .importerHeader: return .importerHeader - case let .importer(_, _, _, peer, _, _, _): + case let .importer(_, _, _, peer, _, _, _, _, _): return .importer(peer.id) } } @@ -94,6 +136,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { return false } + case let .subscriptionHeader(lhsTheme, lhsTitle): + if case let .subscriptionHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { + return true + } else { + return false + } + case let .subscriptionPricing(lhsTheme, lhsTitle, lhsSubtitle): + if case let .subscriptionPricing(rhsTheme, rhsTitle, rhsSubtitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle { + return true + } else { + return false + } case let .creatorHeader(lhsTheme, lhsTitle): if case let .creatorHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true @@ -124,8 +178,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } else { return false } - case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading): - if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsJoinedViaFolderLink, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsJoinedViaFolderLink == rhsJoinedViaFolderLink, lhsLoading == rhsLoading { + case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading, lhsImporter, lhsPricing): + if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsJoinedViaFolderLink, rhsLoading, rhsImporter, rhsPricing) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsJoinedViaFolderLink == rhsJoinedViaFolderLink, lhsLoading == rhsLoading, lhsImporter == rhsImporter, lhsPricing == rhsPricing { return true } else { return false @@ -139,33 +193,47 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { switch rhs { case .link: return false + case .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: + return true + } + case .subscriptionHeader: + switch rhs { + case .link, .subscriptionHeader: + return false + case .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: + return true + } + case .subscriptionPricing: + switch rhs { + case .link, .subscriptionHeader, .subscriptionPricing: + return false case .creatorHeader, .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creatorHeader: switch rhs { - case .link, .creatorHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader: return false case .creator, .requestHeader, .request, .importerHeader, .importer: return true } case .creator: switch rhs { - case .link, .creatorHeader, .creator: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator: return false case .requestHeader, .request, .importerHeader, .importer: return true } case .requestHeader: switch rhs { - case .link, .creatorHeader, .creator, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader: return false case .request, .importerHeader, .importer: return true } case let .request(lhsIndex, _, _, _, _, _): switch rhs { - case .link, .creatorHeader, .creator, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader: return false case let .request(rhsIndex, _, _, _, _, _): return lhsIndex < rhsIndex @@ -174,16 +242,16 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { } case .importerHeader: switch rhs { - case .link, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .requestHeader, .request, .importerHeader: return false case .importer: return true } - case let .importer(lhsIndex, _, _, _, _, _, _): + case let .importer(lhsIndex, _, _, _, _, _, _, _, _): switch rhs { - case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: + case .link, .subscriptionHeader, .subscriptionPricing, .creatorHeader, .creator, .importerHeader, .request, .requestHeader: return false - case let .importer(rhsIndex, _, _, _, _, _, _): + case let .importer(rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex < rhsIndex } } @@ -192,7 +260,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { func item(account: Account, presentationData: PresentationData, interaction: InviteLinkViewInteraction) -> ListViewItem { switch self { case let .link(_, invite): - return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: !invite.isRevoked, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: { + return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: !invite.isRevoked, separateButtons: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: { interaction.copyLink(invite) }, shareAction: { if invitationAvailability(invite).isZero { @@ -204,13 +272,22 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { interaction.contextAction(invite, node, gesture) }, viewAction: { }) + case let .subscriptionHeader(_, title): + return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) + case let .subscriptionPricing(_, title, subtitle): + let attributedTitle = NSMutableAttributedString(string: title, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemPrimaryTextColor) + if let range = attributedTitle.string.range(of: "⭐️") { + attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedTitle.string)) + attributedTitle.addAttribute(.baselineOffset, value: -1.0, range: NSRange(range, in: attributedTitle.string)) + } + return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), icon: subscriptionLinkIcon, context: interaction.context, title: "", attributedTitle: attributedTitle, enabled: false, label: subtitle, labelStyle: .detailText, sectionId: 0, style: .plain, disclosureStyle: .none, noInsets: true, action: nil, clearHighlightAutomatically: true, tag: nil, shimmeringIndex: nil) case let .creatorHeader(_, title): return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) case let .creator(_, dateTimeFormat, peer, date): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil) case let .importerHeader(_, title, subtitle, expired), let .requestHeader(_, title, subtitle, expired): let additionalText: SectionHeaderAdditionalText if !subtitle.isEmpty { @@ -223,21 +300,40 @@ private enum InviteLinkViewEntry: Comparable, Identifiable { additionalText = .none } return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText) - case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading): + case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading, importer, pricing): let dateString: String if joinedViaFolderLink { dateString = presentationData.strings.InviteLink_LabelJoinedViaFolder } else { dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) } - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { - interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + + let label: ItemListPeerItemLabel + if let pricing { + let text = NSMutableAttributedString() + text.append(NSAttributedString(string: "⭐️\(pricing.amount)\n", font: Font.semibold(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)) + text.append(NSAttributedString(string: presentationData.strings.InviteLink_PerMonth, font: Font.regular(13.0), textColor: presentationData.theme.list.itemSecondaryTextColor)) + if let range = text.string.range(of: "⭐️") { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string)) + text.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 3.5, range: NSRange(range, in: text.string)) + } + label = .attributedText(text) + } else { + label = .none + } + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: label, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + if let importer, let pricing { + interaction.openSubscription(pricing, importer) + } else { + interaction.openPeer(peer.id) + } + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) case let .request(_, _, dateTimeFormat, peer, date, loading): let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .peerList, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: { interaction.openPeer(peer.id) - }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) + }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, style: .plain, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil) } } } @@ -424,8 +520,10 @@ public final class InviteLinkViewController: ViewController { self.presentationDataPromise = Promise(self.presentationData) self.controller = controller + let configuration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + self.importersContext = importersContext ?? context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .invite(invite: invite, requested: false)) - if case let .link(_, _, _, requestApproval, _, _, _, _, _, _, _, _) = invite, requestApproval { + if case let .link(_, _, _, requestApproval, _, _, _, _, _, _, _, _, _) = invite, requestApproval { self.requestsContext = context.engine.peers.peerInvitationImporters(peerId: peerId, subject: .invite(invite: invite, requested: true)) } else { self.requestsContext = nil @@ -483,14 +581,26 @@ public final class InviteLinkViewController: ViewController { self.interaction = InviteLinkViewInteraction(context: context, openPeer: { [weak self] peerId in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer else { + guard let peer else { return } - if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always)) } }) + }, openSubscription: { [weak self] pricing, importer in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let peer else { + return + } + var usdRate = 0.012 + if let usdWithdrawRate = configuration.usdWithdrawRate { + usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 + } + let subscriptionController = context.sharedContext.makeStarsSubscriptionScreen(context: context, peer: peer, pricing: pricing, importer: importer, usdRate: usdRate) + self?.controller?.push(subscriptionController) + }) }, copyLink: { [weak self] invite in UIPasteboard.general.string = invite.link @@ -568,7 +678,7 @@ public final class InviteLinkViewController: ViewController { } var creatorIsBot: Signal - if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _) = invite { + if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite { creatorIsBot = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: adminId)) |> map { peer -> Bool in if let peer, case let .user(user) = peer, user.botInfo != nil { @@ -699,7 +809,7 @@ public final class InviteLinkViewController: ViewController { }))) } } - + let contextController = ContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }) @@ -715,8 +825,8 @@ public final class InviteLinkViewController: ViewController { } else { requestsState = .single(PeerInvitationImportersState.Empty) } - - if case let .link(_, _, _, _, _, adminId, date, _, _, usageLimit, _, _) = invite { + + if case let .link(_, _, _, _, _, adminId, date, _, _, usageLimit, _, _, _) = invite { self.disposable = (combineLatest( self.presentationDataPromise.get(), self.importersContext.state, @@ -724,9 +834,27 @@ public final class InviteLinkViewController: ViewController { context.account.postbox.loadedPeerWithId(adminId) ) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in if let strongSelf = self { + var usdRate = 0.012 + if let usdWithdrawRate = configuration.usdWithdrawRate { + usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 + } + var entries: [InviteLinkViewEntry] = [] entries.append(.link(presentationData.theme, invite)) + + if let pricing = invite.pricing { + entries.append(.subscriptionHeader(presentationData.theme, presentationData.strings.InviteLink_SubscriptionFee_Title.uppercased())) + var title = presentationData.strings.InviteLink_SubscriptionFee_PerMonth("\(pricing.amount)").string + var subtitle = presentationData.strings.InviteLink_SubscriptionFee_NoOneJoined + if state.count > 0 { + title += " x \(state.count)" + let usdValue = formatTonUsdValue(pricing.amount * Int64(state.count), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat) + subtitle = presentationData.strings.InviteLink_SubscriptionFee_ApproximateIncome(usdValue).string + } + entries.append(.subscriptionPricing(presentationData.theme, title, subtitle)) + } + entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased())) entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date)) @@ -776,14 +904,14 @@ public final class InviteLinkViewController: ViewController { loading = true let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil) for i in 0 ..< count { - entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true)) + entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true, nil, nil)) } } else { count = min(4, Int32(state.importers.count)) loading = false for importer in state.importers { if let peer = importer.peer.peer { - entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, importer.joinedViaFolderLink, false)) + entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, importer.joinedViaFolderLink, false, importer, invite.pricing)) } index += 1 } @@ -1002,7 +1130,7 @@ public final class InviteLinkViewController: ViewController { var subtitleText = "" var subtitleColor = self.presentationData.theme.list.itemSecondaryTextColor - if case let .link(_, title, _, _, isRevoked, _, _, _, expireDate, usageLimit, count, _) = self.invite { + if case let .link(_, title, _, _, isRevoked, _, _, _, expireDate, usageLimit, count, _, _) = self.invite { if isRevoked { subtitleText = self.presentationData.strings.InviteLink_Revoked } else if let usageLimit = usageLimit, let count = count, count >= usageLimit { diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 2c5f76a94b0..2b592ce5abb 100644 --- a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift +++ b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift @@ -198,7 +198,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio } else { string = presentationData.strings.MemberRequests_UserAddedToGroup(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: peer, title: nil, text: string, action: nil, duration: 3), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) }) } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index 1abb2b9da89..c1c9a6e3cbd 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -7,9 +7,12 @@ import TelegramPresentationData import ItemListUI import ShimmerEffect import TelegramCore +import TextNodeWithEntities +import AccountContext +import TextFormat func invitationAvailability(_ invite: ExportedInvitation) -> CGFloat { - if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _) = invite { + if case let .link(_, _, _, _, isRevoked, _, date, startDate, expireDate, usageLimit, count, _, _) = invite { if isRevoked { return 0.0 } @@ -42,7 +45,7 @@ private enum ItemBackgroundColor: Equatable { case .blue: return (UIColor(rgb: 0x00b5f7), UIColor(rgb: 0x00b2f6), UIColor(rgb: 0xa7f4ff)) case .green: - return (UIColor(rgb: 0x4aca62), UIColor(rgb: 0x43c85c), UIColor(rgb: 0xc5ffe6)) + return (UIColor(rgb: 0x31b73b), UIColor(rgb: 0x88d93b), UIColor(rgb: 0xc5ffe6)) case .yellow: return (UIColor(rgb: 0xf8a953), UIColor(rgb: 0xf7a64e), UIColor(rgb: 0xfeffd7)) case .red: @@ -54,6 +57,7 @@ private enum ItemBackgroundColor: Equatable { } public class ItemListInviteLinkItem: ListViewItem, ItemListItem { + let context: AccountContext let presentationData: ItemListPresentationData let invite: ExportedInvitation? let share: Bool @@ -64,6 +68,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem { public let tag: ItemListItemTag? public init( + context: AccountContext, presentationData: ItemListPresentationData, invite: ExportedInvitation?, share: Bool, @@ -73,6 +78,7 @@ public class ItemListInviteLinkItem: ListViewItem, ItemListItem { contextAction: ((ExportedInvitation, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil ) { + self.context = context self.presentationData = presentationData self.invite = invite self.share = share @@ -170,11 +176,13 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { private let titleNode: TextNode private let subtitleNode: TextNode + private let pricingNode: TextNodeWithEntities private var placeholderNode: ShimmerEffectNode? private var absoluteLocation: (CGRect, CGSize)? private var currentColor: ItemBackgroundColor? + private var currentIsPaid: Bool? private var layoutParams: (ItemListInviteLinkItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)? public var tag: ItemListItemTag? @@ -201,7 +209,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.iconBackgroundNode = ASDisplayNode() self.iconBackgroundNode.setLayerBlock { () -> CALayer in - return CAShapeLayer() + return CAGradientLayer() } self.iconNode = ASImageNode() @@ -218,6 +226,8 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.contentMode = .left self.subtitleNode.contentsScale = UIScreen.main.scale + + self.pricingNode = TextNodeWithEntities() self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -237,6 +247,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self.offsetContainerNode.addSubnode(self.iconNode) self.offsetContainerNode.addSubnode(self.titleNode) self.offsetContainerNode.addSubnode(self.subtitleNode) + self.offsetContainerNode.addSubnode(self.pricingNode.textNode) self.containerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self, let item = strongSelf.layoutParams?.0, let invite = item.invite, let contextAction = item.contextAction else { @@ -266,20 +277,25 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { self?.extractedBackgroundImageNode.image = nil } }) + transition.updateAlpha(node: strongSelf.pricingNode.textNode, alpha: isExtracted ? 0.0 : 1.0) } } public override func didLoad() { super.didLoad() - if let shapeLayer = self.iconBackgroundNode.layer as? CAShapeLayer { - shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0)).cgPath + self.iconBackgroundNode.cornerRadius = 20.0 + if let iconBackgroundLayer = self.iconBackgroundNode.layer as? CAGradientLayer { + iconBackgroundLayer.startPoint = CGPoint(x: 0.0, y: 0.0) + iconBackgroundLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + iconBackgroundLayer.type = .axial } } public func asyncLayout() -> (_ item: ItemListInviteLinkItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let makePricingLayout = TextNodeWithEntities.asyncLayout(self.pricingNode) let currentItem = self.layoutParams?.0 @@ -299,14 +315,19 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let color: ItemBackgroundColor let nextColor: ItemBackgroundColor let transitionFraction: CGFloat - if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _) = invite { + if let invite = item.invite, case let .link(_, _, _, _, isRevoked, _, _, _, expireDate, usageLimit, _, _, pricing) = invite { if isRevoked { color = .gray nextColor = .gray transitionFraction = 0.0 } else if expireDate == nil && usageLimit == nil { - color = .blue - nextColor = .blue + if let _ = pricing { + color = .green + nextColor = .green + } else { + color = .blue + nextColor = .blue + } transitionFraction = 0.0 } else if availability >= 0.5 { color = .green @@ -327,26 +348,33 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { transitionFraction = 0.0 } - let topColor = color.colors.top - let nextTopColor = nextColor.colors.top - let iconColor: UIColor + let colors = color.colors + let nextColors = nextColor.colors + let topIconColor: UIColor + let bottomIconColor: UIColor if let _ = item.invite { - if case .blue = color { - iconColor = item.presentationData.theme.list.itemAccentColor + if case .green = color, item.invite?.pricing != nil { + topIconColor = color.colors.bottom + bottomIconColor = color.colors.top + } else if case .blue = color { + topIconColor = item.presentationData.theme.list.itemAccentColor + bottomIconColor = topIconColor } else { - iconColor = nextTopColor.mixedWith(topColor, alpha: transitionFraction) + topIconColor = nextColors.top.mixedWith(colors.top, alpha: transitionFraction) + bottomIconColor = topIconColor } } else { - iconColor = item.presentationData.theme.list.mediaPlaceholderColor + topIconColor = item.presentationData.theme.list.mediaPlaceholderColor + bottomIconColor = topIconColor } let inviteLink = item.invite?.link?.replacingOccurrences(of: "https://", with: "") ?? "" var titleText = inviteLink var subtitleText: String = "" + var pricingAttributedText: NSMutableAttributedString? var timerValue: TimerNode.Value? - - if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount) = invite { + if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite { if let title = title, !title.isEmpty { titleText = title } @@ -375,6 +403,18 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { subtitleText += item.presentationData.strings.MemberRequests_PeopleRequestedShort(requestedCount) } + if let subscriptionPricing { + let text = NSMutableAttributedString() + text.append(NSAttributedString(string: "⭐️\(subscriptionPricing.amount)\n", font: Font.semibold(17.0), textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + text.append(NSAttributedString(string: item.presentationData.strings.InviteLink_PerMonth, font: Font.regular(13.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor)) + if let range = text.string.range(of: "⭐️") { + text.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: text.string)) + text.addAttribute(NSAttributedString.Key.font, value: Font.semibold(15.0), range: NSRange(range, in: text.string)) + text.addAttribute(.baselineOffset, value: 3.5, range: NSRange(range, in: text.string)) + } + pricingAttributedText = text + } + if invite.isRevoked { if !subtitleText.isEmpty { subtitleText += " • " @@ -443,6 +483,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (pricingLayout, pricingApply) = makePricingLayout(TextNodeLayoutArguments(attributedString: pricingAttributedText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .right, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let titleSpacing: CGFloat = 1.0 @@ -495,8 +536,11 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.contextSourceNode.contentRect = extractedRect - if let layer = strongSelf.iconBackgroundNode.layer as? CAShapeLayer { - layer.fillColor = iconColor.cgColor + if let iconBackgroundLayer = strongSelf.iconBackgroundNode.layer as? CAGradientLayer { + iconBackgroundLayer.colors = [ + topIconColor.cgColor, + bottomIconColor.cgColor + ] } if let _ = updatedTheme { @@ -504,14 +548,23 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - - strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } + + let isPaid = item.invite?.pricing != nil + if updatedTheme != nil || strongSelf.currentIsPaid != isPaid { + strongSelf.currentIsPaid = isPaid + if isPaid { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/SubscriptionLink"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } else { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/InviteLink"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } } let transition = ContainedViewLayoutTransition.immediate let _ = titleApply() let _ = subtitleApply() + let _ = pricingApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, attemptSynchronous: false)) switch item.style { case .plain: @@ -597,7 +650,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { strongSelf.timerNode = timerNode strongSelf.offsetContainerNode.addSubnode(timerNode) } - timerNode.update(color: iconColor, value: timerValue) + timerNode.update(color: topIconColor, value: timerValue) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil timerNode.removeFromSupernode() @@ -607,6 +660,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)) + transition.updateFrame(node: strongSelf.pricingNode.textNode, frame: CGRect(origin: CGPoint(x: layout.contentSize.width - rightInset - pricingLayout.size.width, y: floorToScreenPixels((layout.contentSize.height - pricingLayout.size.height) / 2.0)), size: pricingLayout.size)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 4c0dee3641f..35dc5ed1b8d 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -32,6 +32,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { let count: Int32 let peers: [EnginePeer] let displayButton: Bool + let separateButtons: Bool let displayImporters: Bool let buttonColor: UIColor? public let sectionId: ItemListSectionId @@ -49,6 +50,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { count: Int32, peers: [EnginePeer], displayButton: Bool, + separateButtons: Bool = false, displayImporters: Bool, buttonColor: UIColor?, sectionId: ItemListSectionId, @@ -65,6 +67,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem { self.count = count self.peers = peers self.displayButton = displayButton + self.separateButtons = separateButtons self.displayImporters = displayImporters self.buttonColor = buttonColor self.sectionId = sectionId @@ -126,6 +129,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem private let addressButtonNode: HighlightTrackingButtonNode private let addressButtonIconNode: ASImageNode private var addressShimmerNode: ShimmerEffectNode? + private var copyButtonNode: SolidRoundedButtonNode? private var shareButtonNode: SolidRoundedButtonNode? private let avatarsButtonNode: HighlightTrackingButtonNode @@ -234,6 +238,11 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } } } + self.copyButtonNode?.pressed = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + item.copyAction?() + } + } self.shareButtonNode?.pressed = { [weak self] in if let strongSelf = self, let item = strongSelf.item { item.shareAction?() @@ -444,7 +453,31 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem strongSelf.addressButtonNode.isHidden = item.contextAction == nil strongSelf.addressButtonIconNode.isHidden = item.contextAction == nil - + + var effectiveSeparateButtons = item.separateButtons + if let invite = item.invite, invitationAvailability(invite).isZero { + effectiveSeparateButtons = false + } + + let copyButtonNode: SolidRoundedButtonNode + if let currentCopyButtonNode = strongSelf.copyButtonNode { + copyButtonNode = currentCopyButtonNode + } else { + let buttonTheme: SolidRoundedButtonTheme + if let buttonColor = item.buttonColor { + buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } else { + buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) + } + copyButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: 50.0, cornerRadius: 11.0) + copyButtonNode.title = item.presentationData.strings.InviteLink_CopyShort + copyButtonNode.pressed = { [weak self] in + self?.item?.copyAction?() + } + strongSelf.addSubnode(copyButtonNode) + strongSelf.copyButtonNode = copyButtonNode + } + let shareButtonNode: SolidRoundedButtonNode if let currentShareButtonNode = strongSelf.shareButtonNode { shareButtonNode = currentShareButtonNode @@ -459,7 +492,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem if let invite = item.invite, invitationAvailability(invite).isZero { shareButtonNode.title = item.presentationData.strings.InviteLink_ReactivateLink } else { - shareButtonNode.title = item.presentationData.strings.InviteLink_Share + shareButtonNode.title = effectiveSeparateButtons ? item.presentationData.strings.InviteLink_ShareShort : item.presentationData.strings.InviteLink_Share } shareButtonNode.pressed = { [weak self] in self?.item?.shareAction?() @@ -468,9 +501,19 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem strongSelf.shareButtonNode = shareButtonNode } - let buttonWidth = contentSize.width - leftInset - rightInset + let buttonSpacing: CGFloat = 8.0 + var buttonWidth = contentSize.width - leftInset - rightInset + var shareButtonOriginX = leftInset + if effectiveSeparateButtons { + buttonWidth = (buttonWidth - buttonSpacing) / 2.0 + shareButtonOriginX = leftInset + buttonWidth + buttonSpacing + } + + let _ = copyButtonNode.updateLayout(width: buttonWidth, transition: .immediate) + copyButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate) - shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) var totalWidth = invitedPeersLayout.size.width var leftOrigin: CGFloat = floorToScreenPixels((params.width - invitedPeersLayout.size.width) / 2.0) @@ -498,9 +541,15 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem strongSelf.fieldButtonNode.isUserInteractionEnabled = item.invite != nil strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0 + + strongSelf.copyButtonNode?.isUserInteractionEnabled = item.invite != nil + strongSelf.copyButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4 + strongSelf.copyButtonNode?.isHidden = !item.displayButton || !effectiveSeparateButtons + strongSelf.shareButtonNode?.isUserInteractionEnabled = item.invite != nil strongSelf.shareButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4 strongSelf.shareButtonNode?.isHidden = !item.displayButton + strongSelf.avatarsButtonNode.isHidden = !item.displayImporters strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index f3e178a2feb..51b612227ff 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -251,6 +251,7 @@ public enum ItemListPeerItemLabel { case text(String, ItemListPeerItemLabelFont) case disclosure(String) case badge(String) + case attributedText(NSAttributedString) } public struct ItemListPeerItemSwitch { @@ -728,7 +729,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo private var avatarButton: HighlightTrackingButton? private let titleNode: TextNode - private let labelNode: TextNode + private let labelNode: TextNodeWithEntities private let labelBadgeNode: ASImageNode private var labelArrowNode: ASImageNode? private let statusNode: TextNode @@ -829,10 +830,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo self.statusNode.contentMode = .left self.statusNode.contentsScale = UIScreen.main.scale - self.labelNode = TextNode() - self.labelNode.isUserInteractionEnabled = false - self.labelNode.contentMode = .left - self.labelNode.contentsScale = UIScreen.main.scale + self.labelNode = TextNodeWithEntities() self.labelBadgeNode = ASImageNode() self.labelBadgeNode.displayWithoutProcessing = true @@ -850,7 +848,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo self.containerNode.addSubnode(self.avatarNode) self.containerNode.addSubnode(self.titleNode) self.containerNode.addSubnode(self.statusNode) - self.containerNode.addSubnode(self.labelNode) + self.containerNode.addSubnode(self.labelNode.textNode) self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in if let strongSelf = self, let layoutParams = strongSelf.layoutParams { @@ -885,7 +883,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo public func asyncLayout() -> (_ item: ItemListPeerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ headerAtTop: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) - let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) @@ -1156,42 +1154,49 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo editingOffset = 0.0 } + var labelMaximumNumberOfLines = 1 var labelInset: CGFloat = 0.0 + var labelAlignment: NSTextAlignment = .natural var updatedLabelArrowNode: ASImageNode? switch item.label { - case .none: - break - case let .text(text, font): - let selectedFont: UIFont - switch font { - case .standard: - selectedFont = labelFont - case let .custom(value): - selectedFont = value - } - labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) - labelInset += 15.0 - case let .disclosure(text): - if let currentLabelArrowNode = currentLabelArrowNode { - updatedLabelArrowNode = currentLabelArrowNode - } else { - let arrowNode = ASImageNode() - arrowNode.isLayerBacked = true - arrowNode.displayWithoutProcessing = true - arrowNode.displaysAsynchronously = false - arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) - updatedLabelArrowNode = arrowNode - } - labelInset += 40.0 - labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) - case let .badge(text): - labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) - labelInset += 15.0 + case .none: + break + case let .attributedText(text): + labelAttributedString = text + labelInset += 15.0 + labelMaximumNumberOfLines = 2 + labelAlignment = .right + case let .text(text, font): + let selectedFont: UIFont + switch font { + case .standard: + selectedFont = labelFont + case let .custom(value): + selectedFont = value + } + labelAttributedString = NSAttributedString(string: text, font: selectedFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + labelInset += 15.0 + case let .disclosure(text): + if let currentLabelArrowNode = currentLabelArrowNode { + updatedLabelArrowNode = currentLabelArrowNode + } else { + let arrowNode = ASImageNode() + arrowNode.isLayerBacked = true + arrowNode.displayWithoutProcessing = true + arrowNode.displaysAsynchronously = false + arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) + updatedLabelArrowNode = arrowNode + } + labelInset += 40.0 + labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + case let .badge(text): + labelAttributedString = NSAttributedString(string: text, font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + labelInset += 15.0 } labelInset += reorderInset - let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: labelMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: labelAlignment, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) let constrainedTitleSize = CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset - labelLayout.size.width - labelInset - titleIconsWidth, height: CGFloat.greatestFiniteMagnitude) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedTitleSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -1351,9 +1356,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let _ = titleApply() let _ = statusApply() - let _ = labelApply() - - strongSelf.labelNode.isHidden = labelAttributedString == nil + if case let .account(context) = item.context { + let _ = labelApply(TextNodeWithEntities.Arguments(context: context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, attemptSynchronous: false)) + } else { + let _ = labelApply(nil) + } + strongSelf.labelNode.textNode.isHidden = labelAttributedString == nil if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -1496,15 +1504,15 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let labelFrame: CGRect if case .badge = item.label { labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) - strongSelf.labelNode.frame = labelFrame + strongSelf.labelNode.textNode.frame = labelFrame } else { labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) - transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame) + transition.updateFrame(node: strongSelf.labelNode.textNode, frame: labelFrame) } if let updateBadgeImage = updatedLabelBadgeImage { if strongSelf.labelBadgeNode.supernode == nil { - strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode) + strongSelf.containerNode.insertSubnode(strongSelf.labelBadgeNode, belowSubnode: strongSelf.labelNode.textNode) } strongSelf.labelBadgeNode.image = updateBadgeImage } @@ -1853,16 +1861,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } let badgeDiameter: CGFloat = 20.0 - let labelSize = self.labelNode.frame.size + let labelSize = self.labelNode.textNode.frame.size let badgeWidth = max(badgeDiameter, labelSize.width + 10.0) let labelFrame: CGRect if case .badge = item.label { - labelFrame = CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: self.labelNode.frame.minY), size: labelSize) + labelFrame = CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelSize.width) / 2.0, y: self.labelNode.textNode.frame.minY), size: labelSize) } else { - labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.bounds.size.width - rightLabelInset, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size) + labelFrame = CGRect(origin: CGPoint(x: offset + params.width - self.labelNode.textNode.bounds.size.width - rightLabelInset, y: self.labelNode.textNode.frame.minY), size: self.labelNode.textNode.bounds.size) } - transition.updateFrame(node: self.labelNode, frame: labelFrame) + transition.updateFrame(node: self.labelNode.textNode, frame: labelFrame) transition.updateFrame(node: self.labelBadgeNode, frame: CGRect(origin: CGPoint(x: offset + params.width - rightLabelInset - badgeWidth, y: self.labelBadgeNode.frame.minY), size: CGSize(width: badgeWidth, height: badgeDiameter))) diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index d830858c1f2..cd8408de513 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -8,6 +8,7 @@ import ShimmerEffect import AvatarNode import TelegramCore import AccountContext +import TextNodeWithEntities private let avatarFont = avatarPlaceholderFont(size: 16.0) @@ -64,12 +65,13 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let style: ItemListStyle let disclosureStyle: ItemListDisclosureStyle + let noInsets: Bool let action: (() -> Void)? let clearHighlightAutomatically: Bool public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { self.presentationData = presentationData self.icon = icon self.context = context @@ -88,6 +90,7 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem { self.sectionId = sectionId self.style = style self.disclosureStyle = disclosureStyle + self.noInsets = noInsets self.action = action self.clearHighlightAutomatically = clearHighlightAutomatically self.tag = tag @@ -151,7 +154,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { var avatarNode: AvatarNode? let iconNode: ASImageNode - let titleNode: TextNode + let titleNode: TextNodeWithEntities let titleIconNode: ASImageNode public let labelNode: TextNode var additionalDetailLabelNode: TextNode? @@ -196,8 +199,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { self.iconNode.isLayerBacked = true self.iconNode.displaysAsynchronously = false - self.titleNode = TextNode() - self.titleNode.isUserInteractionEnabled = false + self.titleNode = TextNodeWithEntities() + self.titleNode.textNode.isUserInteractionEnabled = false self.titleIconNode = ASImageNode() self.titleIconNode.displayWithoutProcessing = true @@ -224,7 +227,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.titleNode) + self.addSubnode(self.titleNode.textNode) self.addSubnode(self.labelNode) self.addSubnode(self.arrowNode) @@ -252,7 +255,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) + let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeAdditionalDetailLabelLayout = TextNode.asyncLayout(self.additionalDetailLabelNode) @@ -329,14 +333,14 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let contentSize: CGSize - let insets: UIEdgeInsets + var insets: UIEdgeInsets let separatorHeight = UIScreenPixel let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor var leftInset = 16.0 + params.leftInset if item.icon != nil { - leftInset += 43.0 + leftInset += item.noInsets ? 49.0 : 43.0 } else if item.iconPeer != nil { leftInset += 46.0 } @@ -370,7 +374,11 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { maxTitleWidth -= 12.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleArguments = TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()) + let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil + let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil + + let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)! let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) @@ -455,6 +463,10 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor contentSize = CGSize(width: params.width, height: height) insets = itemListNeighborsPlainInsets(neighbors) + if item.noInsets { + insets.top = 0.0 + insets.bottom = 0.0 + } case .blocks: itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor @@ -531,8 +543,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } + + if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { + let _ = titleWithEntitiesApply( + TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), + attemptSynchronous: false + ) + ) + } else if let titleApply = titleLayoutAndApply?.1 { + let _ = titleApply() + } - let _ = titleApply() let _ = labelApply() switch item.style { @@ -607,7 +632,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { } let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) - strongSelf.titleNode.frame = titleFrame + strongSelf.titleNode.textNode.frame = titleFrame if let updateBadgeImage = updatedLabelBadgeImage { if strongSelf.labelBadgeNode.supernode == nil { @@ -746,7 +771,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let titleLineWidth: CGFloat = (shimmeringIndex % 2 == 0) ? 120.0 : 80.0 let lineDiameter: CGFloat = 8.0 - let titleFrame = strongSelf.titleNode.frame + let titleFrame = strongSelf.titleNode.textNode.frame shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: contentSize) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift index 5e5fcb35f68..81dc1224b23 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSingleLineInputItem.swift @@ -4,6 +4,8 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData +import TextNodeWithEntities +import AccountContext private let validIdentifierSet: CharacterSet = { var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) @@ -43,10 +45,12 @@ public enum ItemListSingleLineInputAlignment { } public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { + let context: AccountContext? let presentationData: ItemListPresentationData let title: NSAttributedString let text: String let placeholder: String + let label: String? let type: ItemListSingleLineInputItemType let returnKeyType: UIReturnKeyType let alignment: ItemListSingleLineInputAlignment @@ -65,11 +69,13 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let cleared: (() -> Void)? public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + public init(context: AccountContext? = nil, presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, label: String? = nil, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { + self.context = context self.presentationData = presentationData self.title = title self.text = text self.placeholder = placeholder + self.label = label self.type = type self.returnKeyType = returnKeyType self.alignment = alignment @@ -130,11 +136,12 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode - private let titleNode: TextNode + private let titleNode: TextNodeWithEntities private let measureTitleSizeNode: TextNode private let textNode: TextFieldNode private let clearIconNode: ASImageNode private let clearButtonNode: HighlightableButtonNode + private let labelNode: TextNode private var item: ItemListSingleLineInputItem? @@ -154,7 +161,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.maskNode = ASImageNode() - self.titleNode = TextNode() + self.titleNode = TextNodeWithEntities() self.measureTitleSizeNode = TextNode() self.textNode = TextFieldNode() @@ -165,12 +172,17 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg self.clearButtonNode = HighlightableButtonNode() + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.titleNode) + self.addSubnode(self.titleNode.textNode) self.addSubnode(self.textNode) self.addSubnode(self.clearIconNode) self.addSubnode(self.clearButtonNode) + self.addSubnode(self.textNode) + self.addSubnode(self.labelNode) self.clearButtonNode.addTarget(self, action: #selector(self.clearButtonPressed), forControlEvents: .touchUpInside) self.clearButtonNode.highligthedChanged = { [weak self] highlighted in @@ -209,8 +221,10 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg } public func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) + let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) let makeMeasureTitleSizeLayout = TextNode.asyncLayout(self.measureTitleSizeNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let currentItem = self.item @@ -241,15 +255,24 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg } let titleString = NSMutableAttributedString(attributedString: item.title) - titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length)) + if !item.title.string.isSingleEmoji { + titleString.removeAttribute(NSAttributedString.Key.font, range: NSMakeRange(0, titleString.length)) + } titleString.addAttributes([NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let titleArguments = TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()) + + let (titleLayoutAndApply) = item.context == nil ? makeTitleLayout(titleArguments) : nil + let (titleWithEntitiesLayoutAndApply) = item.context != nil ? makeTitleWithEntitiesLayout(titleArguments) : nil + + let titleLayout: TextNodeLayout = (titleWithEntitiesLayoutAndApply?.0 ?? titleLayoutAndApply?.0)! let (measureTitleLayout, measureTitleSizeApply) = makeMeasureTitleSizeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "A", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let separatorHeight = UIScreenPixel + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label ?? "", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let separatorHeight = UIScreenPixel + let contentSize = CGSize(width: params.width, height: max(titleLayout.size.height, measureTitleLayout.size.height) + 22.0) let insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -280,10 +303,23 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor } - let _ = titleApply() - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { + let _ = titleWithEntitiesApply( + TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: item.presentationData.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), + attemptSynchronous: false + ) + ) + } else if let titleApply = titleLayoutAndApply?.1 { + let _ = titleApply() + } + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) let _ = measureTitleSizeApply() + let _ = labelApply() let secureEntry: Bool let capitalizationType: UITextAutocapitalizationType @@ -353,6 +389,8 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + titleLayout.size.width + item.spacing, y: 0.0), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset + titleLayout.size.width + item.spacing)), height: layout.contentSize.height - 2.0)) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: layoutSize.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + switch item.alignment { case .default: strongSelf.textNode.textField.textAlignment = .natural diff --git a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift index f42594e4f0f..fd12587163b 100644 --- a/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift +++ b/submodules/PasswordSetupUI/Sources/ResetPasswordController.swift @@ -11,10 +11,12 @@ import AlertUI import PresentationDataUtils private final class ResetPasswordControllerArguments { + let context: AccountContext let updateCodeText: (String) -> Void let openHelp: () -> Void - init(updateCodeText: @escaping (String) -> Void, openHelp: @escaping () -> Void) { + init(context: AccountContext, updateCodeText: @escaping (String) -> Void, openHelp: @escaping () -> Void) { + self.context = context self.updateCodeText = updateCodeText self.openHelp = openHelp } @@ -128,7 +130,7 @@ public func resetPasswordController(context: AccountContext, emailPattern: Strin let saveDisposable = MetaDisposable() actionsDisposable.add(saveDisposable) - let arguments = ResetPasswordControllerArguments(updateCodeText: { updatedText in + let arguments = ResetPasswordControllerArguments(context: context, updateCodeText: { updatedText in updateState { state in var state = state state.code = updatedText diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 914ed416acf..a97ea37c150 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -580,7 +580,7 @@ private func canEditAdminRights(accountPeerId: EnginePeer.Id, channelPeer: Engin switch initialParticipant { case .creator: return false - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId } else { @@ -703,7 +703,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let currentRightsFlags: TelegramChatAdminRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights } else if let initialParticipant = initialParticipant, case let .creator(_, maybeAdminRights, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights @@ -761,7 +761,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } else { if case let .user(adminPeer) = adminPeer, adminPeer.botInfo != nil, case .group = channel.info, invite, let channelPeer = channelPeer, canEditAdminRights(accountPeerId: accountPeerId, channelPeer: channelPeer, initialParticipant: initialParticipant) { - if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, adminInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _, _) = initialParticipant, adminInfo != nil { } else { entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights)) @@ -784,7 +784,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let currentRightsFlags: TelegramChatAdminRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights } else { currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) @@ -846,14 +846,14 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s canTransfer = true } - if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _, _) = initialParticipant, admin.id != accountPeerId, adminInfo != nil { if channel.flags.contains(.isCreator) { canDismiss = true } else { switch initialParticipant { case .creator: break - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer { canDismiss = true @@ -862,7 +862,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } } - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { var index = 0 rightsLoop: for right in rightsOrder { let enabled: Bool = false @@ -955,7 +955,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s entries.append(.rank(presentationData.theme, presentationData.strings, isCreator ? presentationData.strings.Group_EditAdmin_RankOwnerPlaceholder : presentationData.strings.Group_EditAdmin_RankAdminPlaceholder, currentRank ?? "", rankEnabled)) } else { if case let .user(adminPeer) = adminPeer, adminPeer.botInfo != nil, invite { - if let initialParticipant = initialParticipant, case let .member(_, _, adminRights, _, _) = initialParticipant, adminRights != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, adminRights, _, _, _) = initialParticipant, adminRights != nil { } else { entries.append(.adminRights(presentationData.theme, presentationData.strings.Bot_AddToChat_Add_AdminRights, state.adminRights)) } @@ -988,7 +988,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s let currentRightsFlags: TelegramChatAdminRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _, _) = initialParticipant, let adminRights = maybeAdminRights { currentRightsFlags = adminRights.rights.rights.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) } else { currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous) @@ -1016,7 +1016,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_RankInfo(placeholder).string, invite)) } - if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _) = initialParticipant, admin.id != accountPeerId, let adminInfo { + if let initialParticipant = initialParticipant, case let .member(_, _, adminInfo, _, _, _) = initialParticipant, admin.id != accountPeerId, let adminInfo { var canDismiss = false if accountIsCreator { canDismiss = true @@ -1319,7 +1319,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD case let .creator(_, adminInfo, rank): currentRank = rank currentFlags = adminInfo?.rights.rights ?? maskRightsFlags.subtracting(.canBeAnonymous) - case let .member(_, _, adminInfo, _, rank): + case let .member(_, _, adminInfo, _, rank, _): if updateFlags == nil { if adminInfo?.rights == nil { if channel.flags.contains(.isCreator) { diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index 651f17e7fa4..2289970d803 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -24,9 +24,9 @@ private final class ChannelAdminsControllerArguments { let addAdmin: () -> Void let openAdmin: (ChannelParticipant) -> Void let updateAntiSpamEnabled: (Bool) -> Void - let updateSignMessagesEnabled: (Bool) -> Void + let updateSignaturesAndProfilesEnabled: (Bool, Bool) -> Void - init(context: AccountContext, openRecentActions: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removeAdmin: @escaping (EnginePeer.Id) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void, updateAntiSpamEnabled: @escaping (Bool) -> Void, updateSignMessagesEnabled: @escaping (Bool) -> Void) { + init(context: AccountContext, openRecentActions: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removeAdmin: @escaping (EnginePeer.Id) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void, updateAntiSpamEnabled: @escaping (Bool) -> Void, updateSignaturesAndProfilesEnabled: @escaping (Bool, Bool) -> Void) { self.context = context self.openRecentActions = openRecentActions self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -34,7 +34,7 @@ private final class ChannelAdminsControllerArguments { self.addAdmin = addAdmin self.openAdmin = openAdmin self.updateAntiSpamEnabled = updateAntiSpamEnabled - self.updateSignMessagesEnabled = updateSignMessagesEnabled + self.updateSignaturesAndProfilesEnabled = updateSignaturesAndProfilesEnabled } } @@ -59,7 +59,8 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { case addAdmin(PresentationTheme, String, Bool) case adminsInfo(PresentationTheme, String) - case signMessages(PresentationTheme, String, Bool) + case signMessages(PresentationTheme, String, Bool, Bool) + case showAuthorProfiles(PresentationTheme, String, Bool, Bool) case signMessagesInfo(PresentationTheme, String) var section: ItemListSectionId { @@ -68,7 +69,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { return ChannelAdminsSection.administration.rawValue case .adminsHeader, .adminPeerItem, .addAdmin, .adminsInfo: return ChannelAdminsSection.admins.rawValue - case .signMessages, .signMessagesInfo: + case .signMessages, .showAuthorProfiles, .signMessagesInfo: return ChannelAdminsSection.signMessages.rawValue } } @@ -91,8 +92,10 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { return .peer(participant.peer.id) case .signMessages: return .index(6) - case .signMessagesInfo: + case .showAuthorProfiles: return .index(7) + case .signMessagesInfo: + return .index(9) } } @@ -170,8 +173,14 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } else { return false } - case let .signMessages(lhsTheme, lhsText, lhsValue): - if case let .signMessages(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + case let .signMessages(lhsTheme, lhsText, lhsValue, lhsOtherValue): + if case let .signMessages(rhsTheme, rhsText, rhsValue, rhsOtherValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsOtherValue == rhsOtherValue { + return true + } else { + return false + } + case let .showAuthorProfiles(lhsTheme, lhsText, lhsValue, lhsOtherValue): + if case let .showAuthorProfiles(rhsTheme, rhsText, rhsValue, rhsOtherValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsOtherValue == rhsOtherValue { return true } else { return false @@ -240,6 +249,13 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { default: return true } + case .showAuthorProfiles: + switch rhs { + case .recentActions, .antiSpam, .antiSpamInfo, .adminsHeader, .addAdmin, .adminPeerItem, .adminsInfo, .signMessages, .showAuthorProfiles: + return false + default: + return true + } case .signMessagesInfo: return false } @@ -266,7 +282,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { switch participant.participant { case .creator: peerText = strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { @@ -297,9 +313,13 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { }) case let .adminsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .signMessages(_, text, value): + case let .signMessages(_, text, value, profiles): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in - arguments.updateSignMessagesEnabled(value) + arguments.updateSignaturesAndProfilesEnabled(value, profiles) + }) + case let .showAuthorProfiles(_, text, value, signatures): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateSignaturesAndProfilesEnabled(signatures, value) }) case let .signMessagesInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) @@ -381,7 +401,7 @@ private struct ChannelAdminsControllerState: Equatable { } } -private func channelAdminsControllerEntries(presentationData: PresentationData, accountPeerId: EnginePeer.Id, peer: EnginePeer?, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?, antiSpamAvailable: Bool, antiSpamEnabled: Bool, signMessagesEnabled: Bool) -> [ChannelAdminsEntry] { +private func channelAdminsControllerEntries(presentationData: PresentationData, accountPeerId: EnginePeer.Id, peer: EnginePeer?, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?, antiSpamAvailable: Bool, antiSpamEnabled: Bool, signMessagesEnabled: Bool, showAuthorProfilesEnabled: Bool) -> [ChannelAdminsEntry] { if participants == nil || participants?.count == nil { return [] } @@ -424,14 +444,14 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt @@ -443,7 +463,7 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, case .creator: canEdit = false canOpen = isGroup && peer.flags.contains(.isCreator) - case let .member(id, _, adminInfo, _, _): + case let .member(id, _, adminInfo, _, _, _): if id == accountPeerId { canEdit = false } else if let adminInfo = adminInfo { @@ -475,8 +495,11 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, } if !isGroup && peer.hasPermission(.sendSomething) { - entries.append(.signMessages(presentationData.theme, presentationData.strings.Channel_SignMessages, signMessagesEnabled)) - entries.append(.signMessagesInfo(presentationData.theme, presentationData.strings.Channel_SignMessages_Help)) + entries.append(.signMessages(presentationData.theme, presentationData.strings.Channel_SignMessages, signMessagesEnabled, showAuthorProfilesEnabled)) + if signMessagesEnabled { + entries.append(.showAuthorProfiles(presentationData.theme, presentationData.strings.Channel_ShowAuthors, showAuthorProfilesEnabled, signMessagesEnabled)) + entries.append(.signMessagesInfo(presentationData.theme, presentationData.strings.Channel_ShowAuthorsFooter)) + } } } } else if case let .legacyGroup(peer) = peer { @@ -508,14 +531,14 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case let .member(_, invitedAt, _, _, _): + case let .member(_, invitedAt, _, _, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt @@ -530,7 +553,7 @@ private func channelAdminsControllerEntries(presentationData: PresentationData, } else { canEdit = false } - case let .member(id, _, adminInfo, _, _): + case let .member(id, _, adminInfo, _, _, _): if id == accountPeerId { editable = false } else if let adminInfo = adminInfo { @@ -687,7 +710,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation } if case .legacyGroup = peer { } else { - pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: nil)) + pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: nil, starsState: nil)) } }) }) @@ -741,7 +764,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation switch participant.participant { case .creator: return - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { var canUnban = false if banInfo.restrictedBy != context.account.peerId { @@ -784,11 +807,11 @@ public func channelAdminsController(context: AccountContext, updatedPresentation |> deliverOnMainQueue).start(next: { peerId in updateAntiSpamDisposable.set(context.engine.peers.toggleAntiSpamProtection(peerId: peerId, enabled: value).start()) }) - }, updateSignMessagesEnabled: { value in + }, updateSignaturesAndProfilesEnabled: { signatures, profiles in let _ = (currentPeerId.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in - updateSignMessagesDisposable.set(context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: peerId, enabled: value).start()) + updateSignMessagesDisposable.set(context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: peerId, signaturesEnabled: signatures, profilesEnabled: profiles).start()) }) }) @@ -870,7 +893,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation var peers: [EnginePeer.Id: EnginePeer] = [:] peers[creator.id] = creator peers[peer.id] = peer - result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer._asPeer(), peers: peers.mapValues({ $0._asPeer() }))) + result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer._asPeer(), peers: peers.mapValues({ $0._asPeer() }))) case .member: break } @@ -911,8 +934,10 @@ public func channelAdminsController(context: AccountContext, updatedPresentation } var signMessagesEnabled = false + var showAuthorProfilesEnabled = false if case let .channel(channel) = view.peer, case let .broadcast(info) = channel.info { signMessagesEnabled = info.flags.contains(.messagesShouldHaveSignatures) + showAuthorProfilesEnabled = info.flags.contains(.messagesShouldHaveProfiles) } var rightNavigationButton: ItemListNavigationButton? @@ -986,7 +1011,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(isGroup ? presentationData.strings.ChatAdmins_Title : presentationData.strings.Channel_Management_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: view.peer, state: state, participants: admins, antiSpamAvailable: antiSpamAvailable, antiSpamEnabled: antiSpamEnabled, signMessagesEnabled: signMessagesEnabled), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: view.peer, state: state, participants: admins, antiSpamAvailable: antiSpamAvailable, antiSpamEnabled: antiSpamEnabled, signMessagesEnabled: signMessagesEnabled, showAuthorProfilesEnabled: showAuthorProfilesEnabled), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index bca367086ec..bdd377d1759 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -303,7 +303,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentRightsFlags = banInfo.rights.flags } else { currentRightsFlags = defaultBannedRights.flags @@ -312,7 +312,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentTimeout: Int32 if let updatedTimeout = state.updatedTimeout { currentTimeout = updatedTimeout - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentTimeout = banInfo.rights.untilDate } else { currentTimeout = Int32.max @@ -351,7 +351,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation entries.append(.timeout(presentationData.theme, presentationData.strings.GroupPermission_Duration, currentTimeoutString)) - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _) = initialParticipant, let initialBannedBy = initialBannedBy { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant, let initialBannedBy = initialBannedBy { entries.append(.exceptionInfo(presentationData.theme, presentationData.strings.GroupPermission_AddedInfo(initialBannedBy.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), stringForRelativeSymbolicTimestamp(strings: presentationData.strings, relativeTimestamp: banInfo.timestamp, relativeTo: state.referenceTimestamp, dateTimeFormat: presentationData.dateTimeFormat)).string)) entries.append(.delete(presentationData.theme, presentationData.strings.GroupPermission_Delete)) } @@ -363,7 +363,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = state.updatedFlags { currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentRightsFlags = banInfo.rights.flags } else { currentRightsFlags = defaultBannedRightsFlags @@ -372,7 +372,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation let currentTimeout: Int32 if let updatedTimeout = state.updatedTimeout { currentTimeout = updatedTimeout - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentTimeout = banInfo.rights.untilDate } else { currentTimeout = Int32.max @@ -411,7 +411,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation entries.append(.timeout(presentationData.theme, presentationData.strings.GroupPermission_Duration, currentTimeoutString)) - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _) = initialParticipant, let initialBannedBy = initialBannedBy { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant, let initialBannedBy = initialBannedBy { entries.append(.exceptionInfo(presentationData.theme, presentationData.strings.GroupPermission_AddedInfo(initialBannedBy.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), stringForRelativeSymbolicTimestamp(strings: presentationData.strings, relativeTimestamp: banInfo.timestamp, relativeTo: state.referenceTimestamp, dateTimeFormat: presentationData.dateTimeFormat)).string)) entries.append(.delete(presentationData.theme, presentationData.strings.GroupPermission_Delete)) } @@ -462,7 +462,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen var effectiveRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = state.updatedFlags { effectiveRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _) = initialParticipant { + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant { effectiveRightsFlags = banInfo.rights.flags } else { effectiveRightsFlags = defaultBannedRightsFlags @@ -671,7 +671,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } if updateFlags == nil && updateTimeout == nil { - if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant { + if case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant { if maybeBanInfo == nil { updateFlags = defaultBannedRightsFlags updateTimeout = Int32.max @@ -683,7 +683,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen let currentRightsFlags: TelegramChatBannedRightsFlags if let updatedFlags = updateFlags { currentRightsFlags = updatedFlags - } else if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentRightsFlags = banInfo.rights.flags } else { currentRightsFlags = defaultBannedRightsFlags @@ -692,7 +692,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen let currentTimeout: Int32 if let updateTimeout = updateTimeout { currentTimeout = updateTimeout - } else if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + } else if case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { currentTimeout = banInfo.rights.untilDate } else { currentTimeout = Int32.max @@ -724,7 +724,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } var previousRights: TelegramChatBannedRights? - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _) = initialParticipant, banInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _, _) = initialParticipant, banInfo != nil { previousRights = banInfo?.rights } @@ -825,7 +825,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } let title: String - if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _) = initialParticipant, banInfo != nil { + if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo, _, _) = initialParticipant, banInfo != nil { title = presentationData.strings.GroupPermission_Title } else { title = presentationData.strings.GroupPermission_NewTitle diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index b028a7f8061..6ee7bdeeb8b 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -160,7 +160,7 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { case let .peerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled): var text: ItemListPeerItemText = .none switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { text = .text(strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary) } @@ -306,7 +306,7 @@ public func channelBlacklistController(context: AccountContext, updatedPresentat switch participant.participant { case .creator: return - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo, adminInfo.promotedBy != context.account.peerId { presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_Members_AddBannedErrorAdmin, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) return diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index aa97ed2b2f2..6f6e2f63f5d 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -511,7 +511,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon case .creator: canPromote = false canRestrict = false - case let .member(_, _, adminRights, bannedRights, _): + case let .member(_, _, adminRights, bannedRights, _, _): if channel.hasPermission(.addAdmins) { canPromote = true } else { @@ -740,7 +740,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon case .creator: canPromote = false canRestrict = false - case let .member(_, _, adminRights, bannedRights, _): + case let .member(_, _, adminRights, bannedRights, _, _): if channel.hasPermission(.addAdmins) { canPromote = true } else { @@ -778,7 +778,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if adminInfo != nil { label = presentationData.strings.Channel_Management_LabelEditor } @@ -809,7 +809,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { @@ -822,7 +822,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchBanned: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { var exceptionsString = "" let sendMediaRights = banSendMediaSubList().map { $0.0 } @@ -844,7 +844,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchKicked: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } @@ -972,11 +972,11 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon peers[creator.id] = creator } peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) case .member: var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) } matchingMembers.append(renderedParticipant) } @@ -1057,7 +1057,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if adminInfo != nil { label = presentationData.strings.Channel_Management_LabelEditor } @@ -1073,7 +1073,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case .creator: label = presentationData.strings.Channel_Management_LabelOwner - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if let peer = participant.peers[adminInfo.promotedBy] { if peer.id == participant.peer.id { @@ -1086,7 +1086,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchBanned: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { var exceptionsString = "" let sendMediaRights = banSendMediaSubList().map { $0.0 } @@ -1108,7 +1108,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } case .searchKicked: switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index 5b399fe10ab..c85e72cd03c 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -399,11 +399,11 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { var peers: [EnginePeer.Id: EnginePeer] = [:] peers[creator.id] = creator peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) case .member: var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) } entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, false, false)) diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index e26fd8e2871..59bee118b4e 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -381,7 +381,7 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { case let .peerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, editing, enabled, canOpen, defaultBannedRights): var text: ItemListPeerItemText = .none switch participant.participant { - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): var exceptionsString = "" if let banInfo = banInfo { let sendMediaRights = banSendMediaSubList().map { $0.0 } @@ -937,7 +937,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent switch participant.participant { case .creator: return - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo, adminInfo.promotedBy != context.account.peerId { presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Channel_Members_AddBannedErrorAdmin, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) return diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift index 589e98d9bc3..cc328ee9892 100644 --- a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -538,7 +538,7 @@ public func peerAllowedReactionListController( } if initialAllowedReactions != .known(updatedValue) { - let _ = context.engine.peers.updatePeerReactionSettings(peerId: peerId, reactionSettings: PeerReactionSettings(allowedReactions: updatedValue, maxReactionCount: 11)).start() + let _ = context.engine.peers.updatePeerReactionSettings(peerId: peerId, reactionSettings: PeerReactionSettings(allowedReactions: updatedValue, maxReactionCount: 11, starsAllowed: nil)).start() } }) } diff --git a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift index 022b44c7633..d7d306b0e0f 100644 --- a/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift +++ b/submodules/PlatformRestrictionMatching/Sources/PlatformRestrictionMatching.swift @@ -21,6 +21,9 @@ public extension RestrictedContentMessageAttribute { // MARK: Nicegram (extractReason) func platformText(platform: String, contentSettings: ContentSettings, extractReason: Bool = false) -> String? { for rule in self.rules { + if rule.reason == "sensitive" { + continue + } if rule.platform == "all" || rule.platform == "ios" || contentSettings.addContentRestrictionReasons.contains(rule.platform) { if !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { // MARK: Nicegram diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index c1832a00f89..bb7a8c3a9db 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -38,6 +38,7 @@ swift_library( "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", "//submodules/Components/BalancedTextComponent", "//submodules/Markdown", + "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index d007db0582d..5cc21aa8ff6 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -24,6 +24,7 @@ import TextFormat import GZip import BalancedTextComponent import Markdown +import PremiumStarComponent public final class ReactionItem { public struct Reaction: Equatable { @@ -69,6 +70,8 @@ public final class ReactionItem { return .builtin(value) case let .custom(fileId): return .custom(fileId: fileId, file: self.listAnimation) + case .stars: + return .stars } } } @@ -965,6 +968,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { break case .custom: loopIdle = true + case .stars: + break } } } @@ -1668,6 +1673,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { updateReaction = .builtin(value) case let .custom(fileId): updateReaction = .custom(fileId: fileId, file: nil) + case .stars: + updateReaction = .stars } let reactionItem = ReactionItem( @@ -2604,7 +2611,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } } - public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { + public func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: @escaping () -> Void) { self.isAnimatingOutToReaction = true var foundItemNode: ReactionNode? @@ -2615,6 +2622,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } } + foundItemNode?.animateHideEffects() + if let customReactionSource = self.customReactionSource { let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, icon: .none, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false) if let contents = customReactionSource.layer.contents { @@ -2639,6 +2648,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { switchToInlineImmediately = forceSwitchToInlineImmediately case .custom: switchToInlineImmediately = !self.didTriggerExpandedReaction + case .stars: + switchToInlineImmediately = forceSwitchToInlineImmediately } } else { switchToInlineImmediately = !self.didTriggerExpandedReaction @@ -2663,6 +2674,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { var selfTargetBounds = targetView.bounds if case .builtin = itemNode.item.reaction.rawValue { selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } else if case .stars = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.13, dy: -selfTargetBounds.height * 0.13).offsetBy(dx: -0.5, dy: -0.5) } let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) @@ -2875,6 +2888,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } } + onHit?() + if switchToInlineImmediately { afterCompletion() } else { @@ -2892,7 +2907,14 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { }) if !switchToInlineImmediately { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: { + let maxDuration: Double + if case .stars = value { + maxDuration = 3.0 + } else { + maxDuration = 2.0 + } + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, maxDuration * UIView.animationDurationFactor()), execute: { if self.didTriggerExpandedReaction { self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in if let strongSelf = self, strongSelf.didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation { @@ -3283,13 +3305,13 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.isUserInteractionEnabled = false } - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, customEffectResource: MediaResource? = nil, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, playCenterReaction: Bool = true, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { - self.animateReactionSelection(context: context, theme: theme, animationCache: animationCache, reaction: reaction, customEffectResource: customEffectResource, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, playCenterReaction: playCenterReaction, forceSmallEffectAnimation: forceSmallEffectAnimation, hideCenterAnimation: hideCenterAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, customEffectResource: MediaResource? = nil, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, playCenterReaction: Bool = true, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)? = nil, completion: @escaping () -> Void) { + self.animateReactionSelection(context: context, theme: theme, animationCache: animationCache, reaction: reaction, customEffectResource: customEffectResource, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, playCenterReaction: playCenterReaction, forceSmallEffectAnimation: forceSmallEffectAnimation, hideCenterAnimation: hideCenterAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, onHit: onHit, completion: completion) } public var currentDismissAnimation: (() -> Void)? - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, customEffectResource: MediaResource? = nil, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, playCenterReaction: Bool = true, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, customEffectResource: MediaResource? = nil, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, playCenterReaction: Bool = true, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, onHit: (() -> Void)? = nil, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return @@ -3298,6 +3320,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if playHaptic { self.hapticFeedback.tap() } + onHit?() self.targetView = targetView @@ -3315,6 +3338,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } let switchToInlineImmediately: Bool + var playAnimationInline = false if let itemNode { if itemNode.item.listAnimation.isVideoEmoji || itemNode.item.listAnimation.isVideoSticker || itemNode.item.listAnimation.isAnimatedSticker || itemNode.item.listAnimation.isStaticEmoji { switch itemNode.item.reaction.rawValue { @@ -3322,6 +3346,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { switchToInlineImmediately = false case .custom: switchToInlineImmediately = true + case .stars: + switchToInlineImmediately = true + playAnimationInline = true } } else { switchToInlineImmediately = false @@ -3329,6 +3356,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } else { switchToInlineImmediately = false } + let _ = playAnimationInline if let itemNode, !forceSmallEffectAnimation, !switchToInlineImmediately, !hideCenterAnimation { if let targetView = targetView as? ReactionIconView, !isLarge { @@ -3366,6 +3394,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode { var expandedSize: CGSize = selfTargetRect.size if isLarge { expandedSize = CGSize(width: 120.0, height: 120.0) + } else if case .stars = reaction.reaction.rawValue { + expandedSize = CGSize(width: 120.0, height: 120.0) } let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize) @@ -3374,6 +3404,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode { let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 if isLarge && !forceSmallEffectAnimation { effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5).offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) + } else if case .stars = reaction.reaction.rawValue { + effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5) } else { effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height) } @@ -3403,6 +3435,8 @@ public final class StandaloneReactionAnimation: ASDisplayNode { var additionalAnimationResource: MediaResource? if isLarge && !forceSmallEffectAnimation { additionalAnimationResource = reaction.largeApplicationAnimation?.resource + } else if case .stars = reaction.reaction.rawValue { + additionalAnimationResource = reaction.largeApplicationAnimation?.resource ?? reaction.applicationAnimation?.resource } else { additionalAnimationResource = reaction.applicationAnimation?.resource } @@ -3708,12 +3742,16 @@ public final class StandaloneReactionAnimation: ASDisplayNode { var selfTargetBounds = targetView.bounds if let itemNode = self.itemNode, case .builtin = itemNode.item.reaction.rawValue { selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + } else if let itemNode = self.itemNode, case .stars = itemNode.item.reaction.rawValue { + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.0, dy: -selfTargetBounds.height * 0.0) } var targetFrame = self.view.convert(targetView.convert(selfTargetBounds, to: nil), from: nil) if let itemNode = self.itemNode, case .builtin = itemNode.item.reaction.rawValue { targetFrame = targetFrame.insetBy(dx: -targetFrame.width * 0.5, dy: -targetFrame.height * 0.5) + } else if let itemNode = self.itemNode, case .stars = itemNode.item.reaction.rawValue { + targetFrame = targetFrame.insetBy(dx: -targetFrame.width * 0.0, dy: -targetFrame.height * 0.0) } targetSnapshotView.frame = targetFrame @@ -3759,6 +3797,251 @@ public final class StandaloneReactionAnimation: ASDisplayNode { itemNode.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 1.0) / itemNode.bounds.width, duration: duration, removeOnCompletion: false) } + public func animateOutToReaction(context: AccountContext, theme: PresentationTheme, item: ReactionItem, value: MessageReaction.Reaction, sourceView: UIView, targetView: UIView, hideNode: Bool, forceSwitchToInlineImmediately: Bool = false, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: @escaping () -> Void) { + let star = ComponentView() + let starSize = star.update( + transition: .immediate, + component: AnyComponent(StandalonePremiumStarComponent( + theme: theme, + colors: [ + UIColor(rgb: 0xe57d02), + UIColor(rgb: 0xf09903), + UIColor(rgb: 0xf9b004), + UIColor(rgb: 0xfdd219) + ] + )), + environment: {}, + containerSize: CGSize(width: 240.0, height: 240.0) + ) + guard let starView = star.view else { + return + } + + guard let sourceCloneView = sourceView.snapshotContentTree() else { + return + } + + self.view.addSubview(sourceCloneView) + self.view.addSubview(starView) + + sourceCloneView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, removeOnCompletion: false) + starView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + + let didTriggerExpandedReaction = "".isEmpty + + let sourceFrame = sourceView.convert(sourceView.bounds, to: self.view) + starView.bounds = CGRect(origin: CGPoint(), size: starSize) + + sourceView.layer.isHidden = true + + let switchToInlineImmediately: Bool = "".isEmpty + + if hideNode { + if let animateTargetContainer = animateTargetContainer { + animateTargetContainer.isHidden = true + targetView.isHidden = true + } else { + targetView.alpha = 0.0 + targetView.layer.animateAlpha(from: targetView.alpha, to: 0.0, duration: 0.2) + } + } + if let targetView = targetView as? ReactionIconView { + targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) + } + + let selfSourceRect = sourceFrame + + var selfTargetBounds = targetView.bounds + selfTargetBounds = selfTargetBounds.insetBy(dx: -selfTargetBounds.width * 0.5, dy: -selfTargetBounds.height * 0.5) + + let selfTargetRect = self.view.convert(selfTargetBounds, from: targetView) + + var expandedSize: CGSize = selfTargetRect.size + if didTriggerExpandedReaction { + expandedSize = CGSize(width: 120.0, height: 120.0) + } + + let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize) + + var effectFrame: CGRect + let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 + if didTriggerExpandedReaction { + let expandFactor: CGFloat = 0.5 + effectFrame = expandedFrame.insetBy(dx: -expandedFrame.width * expandFactor, dy: -expandedFrame.height * expandFactor)//.offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0) + } else { + effectFrame = expandedFrame.insetBy(dx: -expandedSize.width, dy: -expandedSize.height) + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .linear) + + starView.center = expandedFrame.center + sourceCloneView.frame = sourceFrame + + let additionalAnimationNode: DefaultAnimatedStickerNodeImpl? + var genericAnimationView: AnimationView? + + var additionalAnimation: TelegramMediaFile? + if didTriggerExpandedReaction { + additionalAnimation = item.largeApplicationAnimation + } else { + additionalAnimation = item.applicationAnimation + } + + if let additionalAnimation = additionalAnimation { + let additionalAnimationNodeValue = DefaultAnimatedStickerNodeImpl() + additionalAnimationNode = additionalAnimationNodeValue + if didTriggerExpandedReaction { + if incomingMessage { + additionalAnimationNodeValue.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + } + + additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimation.resource.id))) + additionalAnimationNodeValue.frame = effectFrame + additionalAnimationNodeValue.updateLayout(size: effectFrame.size) + self.addSubnode(additionalAnimationNodeValue) + } else if item.isCustom { + additionalAnimationNode = nil + + var effectData: Data? + if didTriggerExpandedReaction { + if let url = getAppBundle().url(forResource: "generic_reaction_effect", withExtension: "json") { + effectData = try? Data(contentsOf: url) + } + } else if let genericReactionEffect = self.genericReactionEffect, let data = try? Data(contentsOf: URL(fileURLWithPath: genericReactionEffect)) { + effectData = TGGUnzipData(data, 5 * 1024 * 1024) ?? data + } else { + if let url = getAppBundle().url(forResource: "generic_reaction_small_effect", withExtension: "json") { + effectData = try? Data(contentsOf: url) + } + } + + if let effectData = effectData, let composition = try? Animation.from(data: effectData) { + let view = AnimationView(animation: composition, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) + view.animationSpeed = 1.0 + view.backgroundColor = nil + view.isOpaque = false + + if incomingMessage { + view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + + genericAnimationView = view + + if didTriggerExpandedReaction { + view.frame = effectFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: incomingMessage ? 22.0 : -22.0, dy: 0.0) + } else { + view.frame = effectFrame.insetBy(dx: -20.0, dy: -20.0) + } + self.view.addSubview(view) + } + } else { + additionalAnimationNode = nil + } + + var mainAnimationCompleted = false + var additionalAnimationCompleted = false + let intermediateCompletion: () -> Void = { + if mainAnimationCompleted && additionalAnimationCompleted { + completion() + } + } + + if let additionalAnimationNode = additionalAnimationNode { + additionalAnimationNode.completed = { _ in + additionalAnimationCompleted = true + intermediateCompletion() + } + } else if let genericAnimationView = genericAnimationView { + genericAnimationView.play(completion: { _ in + additionalAnimationCompleted = true + intermediateCompletion() + }) + } else { + additionalAnimationCompleted = true + } + + starView.center = selfTargetRect.center + sourceCloneView.center = selfTargetRect.center + + let starSourceScale = sourceFrame.width / starSize.width + let starDestinationScale = selfTargetRect.width / starSize.width + + let elevation: CGFloat = min(selfSourceRect.center.y, expandedFrame.center.y) - selfSourceRect.center.y - 40.0 + let keyframes = generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: -elevation) + let scaleKeyframes = generateScaleKeyframes(from: starSourceScale, center: 1.0, to: starDestinationScale) + starView.layer.transform = CATransform3DMakeScale(starDestinationScale, starDestinationScale, 1.0) + transition.animateScaleWithKeyframes(layer: starView.layer, keyframes: scaleKeyframes) + transition.animatePositionWithKeyframes(layer: starView.layer, keyframes: keyframes, completion: { [weak starView, weak targetView, weak animateTargetContainer] _ in + let afterCompletion: () -> Void = { + guard let starView else { + return + } + if let animateTargetContainer = animateTargetContainer { + animateTargetContainer.isHidden = false + } + + if let targetView = targetView { + targetView.isHidden = false + targetView.alpha = 1.0 + targetView.layer.removeAnimation(forKey: "opacity") + } + + HapticFeedback().tap() + + if let targetView = targetView as? ReactionIconView { + if switchToInlineImmediately { + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) + starView.isHidden = true + } else { + targetView.updateIsAnimationHidden(isAnimationHidden: true, transition: .immediate) + } + } else if let targetView = targetView as? UIImageView { + starView.isHidden = true + targetView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) + targetView.layer.animateScale(from: 0.2, to: 1.0, duration: 0.12) + } + + if switchToInlineImmediately { + mainAnimationCompleted = true + intermediateCompletion() + } + } + + onHit?() + + if switchToInlineImmediately { + afterCompletion() + } else { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: afterCompletion) + } + }) + transition.animatePositionWithKeyframes(layer: sourceCloneView.layer, keyframes: keyframes) + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3 * UIView.animationDurationFactor(), execute: { + additionalAnimationNode?.visibility = true + if let animateTargetContainer = animateTargetContainer { + animateTargetContainer.isHidden = false + animateTargetContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animateTargetContainer.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + }) + + if !switchToInlineImmediately { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: { + if hideNode { + targetView.alpha = 1.0 + targetView.isHidden = false + if let targetView = targetView as? ReactionIconView { + targetView.updateIsAnimationHidden(isAnimationHidden: false, transition: .immediate) + } + } + mainAnimationCompleted = true + intermediateCompletion() + }) + } + } + public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { self.bounds = self.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: self, offset: -offset.y) @@ -3862,3 +4145,24 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar return keyframes } + +private func generateScaleKeyframes(from: CGFloat, center: CGFloat, to: CGFloat) -> [CGFloat] { + var keyframes: [CGFloat] = [] + for i in 0 ..< 10 { + var k = CGFloat(i) / CGFloat(10 - 1) + let valueFrom: CGFloat + let valueTo: CGFloat + if k <= 0.5 { + k /= 0.5 + valueFrom = from + valueTo = center + } else { + k = (k - 0.5) / 0.5 + valueFrom = center + valueTo = to + } + let value = valueFrom * (1.0 - k) + valueTo * k + keyframes.append(value) + } + return keyframes +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 180f7524c98..79bc8bfbee5 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -56,11 +56,13 @@ protocol ReactionItemNode: ASDisplayNode { private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 16.0, color: .white)!.withRenderingMode(.alwaysTemplate) private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) -private final class StarsReactionEffectLayer: SimpleLayer { +private final class StarsButtonEffectLayer: SimpleLayer { + let emitterLayer = CAEmitterLayer() + override init() { super.init() - self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor + self.addSublayer(self.emitterLayer) } override init(layer: Any) { @@ -71,7 +73,45 @@ private final class StarsReactionEffectLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } + private func setup() { + let color = UIColor(rgb: 0xffbe27) + + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 25.0 + emitter.lifetime = 2.0 + emitter.velocity = 12.0 + emitter.velocityRange = 3 + emitter.scale = 0.1 + emitter.scaleRange = 0.08 + emitter.alphaRange = 0.1 + emitter.emissionRange = .pi * 2.0 + emitter.setValue(3.0, forKey: "mass") + emitter.setValue(2.0, forKey: "massRange") + + let staticColors: [Any] = [ + color.withAlphaComponent(0.0).cgColor, + color.cgColor, + color.cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + + self.emitterLayer.emitterCells = [emitter] + } + func update(size: CGSize) { + if self.emitterLayer.emitterCells == nil { + self.setup() + } + self.emitterLayer.emitterShape = .circle + self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7) + self.emitterLayer.emitterMode = .surface + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) } } @@ -88,7 +128,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { let selectionTintView: UIView? let selectionView: UIView? - private var starsEffectLayer: StarsReactionEffectLayer? + private var starsEffectLayer: StarsButtonEffectLayer? private var animateInAnimationNode: AnimatedStickerNode? private var staticAnimationPlaceholderView: UIImageView? @@ -150,8 +190,8 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { super.init() - if case .custom(MessageReaction.starsReactionId) = item.reaction.rawValue { - let starsEffectLayer = StarsReactionEffectLayer() + if case .stars = item.reaction.rawValue { + let starsEffectLayer = StarsButtonEffectLayer() self.starsEffectLayer = starsEffectLayer self.layer.addSublayer(starsEffectLayer) } @@ -256,6 +296,12 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self.customContentsNode?.contents = contents } + public func animateHideEffects() { + if let starsEffectLayer = self.starsEffectLayer { + starsEffectLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + } + public func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) { let intrinsicSize = size diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index f220088e4dc..a32b6c972e6 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -232,6 +232,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal, _ s21: Signal, _ s22: Signal, _ s23: Signal, _ s24: Signal, _ s25: Signal) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25), E> { + return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24), signalOfAny(s25)], combine: { values in + return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24, values[24] as! T25) + }, initialValues: [:], queue: queue) +} + public func combineLatest(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift index 59c2896d0c2..199624f520e 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift @@ -15,10 +15,12 @@ import AuthorizationUtils import PhoneNumberFormat private final class ChangePhoneNumberCodeControllerArguments { + let context: AccountContext let updateEntryText: (String) -> Void let next: () -> Void - init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { + init(context: AccountContext, updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void) { + self.context = context self.updateEntryText = updateEntryText self.next = next } @@ -290,7 +292,7 @@ func changePhoneNumberCodeController(context: AccountContext, phoneNumber: Strin } } - let arguments = ChangePhoneNumberCodeControllerArguments(updateEntryText: { updatedText in + let arguments = ChangePhoneNumberCodeControllerArguments(context: context, updateEntryText: { updatedText in var initiateCheck = false updateState { state in if state.codeText.count < 5 && updatedText.count == 5 { diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index adf77392b48..8ab3cdc71c9 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -13,6 +13,7 @@ import AccountContext import OpenInExternalAppUI import ItemListPeerActionItem import StorageUsageScreen +import PresentationDataUtils public enum AutomaticSaveIncomingPeerType { case privateChats @@ -34,9 +35,24 @@ private final class DataAndStorageControllerArguments { let toggleDownloadInBackground: (Bool) -> Void let openBrowserSelection: () -> Void let openIntents: () -> Void - let toggleEnableSensitiveContent: (Bool) -> Void + let toggleSensitiveContent: (Bool) -> Void - init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openProxy: @escaping () -> Void, openAutomaticDownloadConnectionType: @escaping (AutomaticDownloadConnectionType) -> Void, resetAutomaticDownload: @escaping () -> Void, toggleVoiceUseLessData: @escaping (Bool) -> Void, openSaveIncoming: @escaping (AutomaticSaveIncomingPeerType) -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void, togglePauseMusicOnRecording: @escaping (Bool) -> Void, toggleRaiseToListen: @escaping (Bool) -> Void, toggleDownloadInBackground: @escaping (Bool) -> Void, openBrowserSelection: @escaping () -> Void, openIntents: @escaping () -> Void, toggleEnableSensitiveContent: @escaping (Bool) -> Void) { + init( + openStorageUsage: @escaping () -> Void, + openNetworkUsage: @escaping () -> Void, + openProxy: @escaping () -> Void, + openAutomaticDownloadConnectionType: @escaping (AutomaticDownloadConnectionType) -> Void, + resetAutomaticDownload: @escaping () -> Void, + toggleVoiceUseLessData: @escaping (Bool) -> Void, + openSaveIncoming: @escaping (AutomaticSaveIncomingPeerType) -> Void, + toggleSaveEditedPhotos: @escaping (Bool) -> Void, + togglePauseMusicOnRecording: @escaping (Bool) -> Void, + toggleRaiseToListen: @escaping (Bool) -> Void, + toggleDownloadInBackground: @escaping (Bool) -> Void, + openBrowserSelection: @escaping () -> Void, + openIntents: @escaping () -> Void, + toggleSensitiveContent: @escaping (Bool) -> Void + ) { self.openStorageUsage = openStorageUsage self.openNetworkUsage = openNetworkUsage self.openProxy = openProxy @@ -50,7 +66,7 @@ private final class DataAndStorageControllerArguments { self.toggleDownloadInBackground = toggleDownloadInBackground self.openBrowserSelection = openBrowserSelection self.openIntents = openIntents - self.toggleEnableSensitiveContent = toggleEnableSensitiveContent + self.toggleSensitiveContent = toggleSensitiveContent } } @@ -62,7 +78,7 @@ private enum DataAndStorageSection: Int32 { case voiceCalls case other case connection - case enableSensitiveContent + case sensitiveContent } public enum DataAndStorageEntryTag: ItemListItemTag, Equatable { @@ -72,6 +88,7 @@ public enum DataAndStorageEntryTag: ItemListItemTag, Equatable { case pauseMusicOnRecording case raiseToListen case autoSave(AutomaticSaveIncomingPeerType) + case sensitiveContent public func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? DataAndStorageEntryTag, self == other { @@ -107,9 +124,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case raiseToListen(PresentationTheme, String, Bool) case raiseToListenInfo(PresentationTheme, String) + case sensitiveContent(String, Bool) + case sensitiveContentInfo(String) + case connectionHeader(PresentationTheme, String) case connectionProxy(PresentationTheme, String, String) - case enableSensitiveContent(String, Bool) var section: ItemListSectionId { switch self { @@ -125,10 +144,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return DataAndStorageSection.voiceCalls.rawValue case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: return DataAndStorageSection.other.rawValue + case .sensitiveContent, .sensitiveContentInfo: + return DataAndStorageSection.sensitiveContent.rawValue case .connectionHeader, .connectionProxy: return DataAndStorageSection.connection.rawValue - case .enableSensitiveContent: - return DataAndStorageSection.enableSensitiveContent.rawValue } } @@ -174,12 +193,14 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return 34 case .raiseToListenInfo: return 35 - case .connectionHeader: + case .sensitiveContent: return 36 - case .connectionProxy: + case .sensitiveContentInfo: return 37 - case .enableSensitiveContent: + case .connectionHeader: return 38 + case .connectionProxy: + return 39 } } @@ -293,6 +314,18 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } + case let .sensitiveContent(text, value): + if case .sensitiveContent(text, value) = rhs { + return true + } else { + return false + } + case let .sensitiveContentInfo(text): + if case .sensitiveContentInfo(text) = rhs { + return true + } else { + return false + } case let .downloadInBackground(lhsTheme, lhsText, lhsValue): if case let .downloadInBackground(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true @@ -317,12 +350,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } - case let .enableSensitiveContent(text, value): - if case .enableSensitiveContent(text, value) = rhs { - return true - } else { - return false - } } } @@ -408,6 +435,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry { }, tag: DataAndStorageEntryTag.raiseToListen) case let .raiseToListenInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case let .sensitiveContent(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enableInteractiveChanges: false, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleSensitiveContent(value) + }, tag: DataAndStorageEntryTag.sensitiveContent) + case let .sensitiveContentInfo(text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .downloadInBackground(_, text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleDownloadInBackground(value) @@ -420,10 +453,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openProxy() }) - case let .enableSensitiveContent(text, value): - return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in - arguments.toggleEnableSensitiveContent(value) - }, tag: nil) } } } @@ -588,7 +617,7 @@ private func autosaveLabelAndValue(presentationData: PresentationData, settings: return (label, value) } -private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, defaultWebBrowser: String, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?]) -> [DataAndStorageEntry] { +private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, defaultWebBrowser: String, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?], mediaSettings: MediaDisplaySettings) -> [DataAndStorageEntry] { var entries: [DataAndStorageEntry] = [] entries.append(.storageUsage(presentationData.theme, presentationData.strings.ChatSettings_Cache, dataSizeString(storageUsage, formatting: DataSizeStringFormatting(presentationData: presentationData)))) @@ -627,6 +656,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak)) entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo)) + if let contentSettingsConfiguration = contentSettingsConfiguration, contentSettingsConfiguration.canAdjustSensitiveContent { + entries.append(.sensitiveContent(presentationData.strings.Settings_SensitiveContent, contentSettingsConfiguration.sensitiveContentEnabled)) + entries.append(.sensitiveContentInfo(presentationData.strings.Settings_SensitiveContentInfo)) + } + let proxyValue: String if let proxySettings = data.proxySettings, let activeServer = proxySettings.activeServer, proxySettings.enabled { switch activeServer.connection { @@ -640,13 +674,7 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat } entries.append(.connectionHeader(presentationData.theme, presentationData.strings.ChatSettings_ConnectionType_Title.uppercased())) entries.append(.connectionProxy(presentationData.theme, presentationData.strings.SocksProxySetup_Title, proxyValue)) - - #if DEBUG - if let contentSettingsConfiguration = contentSettingsConfiguration, contentSettingsConfiguration.canAdjustSensitiveContent { - entries.append(.enableSensitiveContent("Display Sensitive Content", contentSettingsConfiguration.sensitiveContentEnabled)) - } - #endif - + return entries } @@ -872,16 +900,30 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da }, openIntents: { let controller = intentsSettingsController(context: context) pushControllerImpl?(controller) - }, toggleEnableSensitiveContent: { value in - let _ = (contentSettingsConfiguration.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak contentSettingsConfiguration] settings in - if var settings = settings { - settings.sensitiveContentEnabled = value - contentSettingsConfiguration?.set(.single(settings)) - } - }) - updateSensitiveContentDisposable.set(updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: value).start()) + }, toggleSensitiveContent: { value in + let update = { + let _ = (contentSettingsConfiguration.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak contentSettingsConfiguration] settings in + if var settings = settings { + settings.sensitiveContentEnabled = value + contentSettingsConfiguration?.set(.single(settings)) + } + }) + updateSensitiveContentDisposable.set(updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: value).start()) + } + if value { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController(context: context, title: presentationData.strings.SensitiveContent_Enable_Title, text: presentationData.strings.SensitiveContent_Enable_Text, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.SensitiveContent_Enable_Confirm, action: { + update() + }) + ]) + presentControllerImpl?(alertController, nil) + } else { + update() + } }) let preferencesKey: PostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings])) @@ -901,11 +943,13 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da )) } + let sensitiveContent = Atomic(value: nil) + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), dataAndStorageDataPromise.get(), - context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), contentSettingsConfiguration.get(), preferences, usageSignal, @@ -913,6 +957,8 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da ) |> map { presentationData, state, dataAndStorageData, sharedData, contentSettingsConfiguration, mediaAutoSaveSettings, usageSignal, autosaveExceptionPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in let webBrowserSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings + let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings + let options = availableOpenInOptions(context: context, item: .url(url: "https://telegram.org")) let defaultWebBrowser: String if let option = options.first(where: { $0.identifier == webBrowserSettings.defaultWebBrowser }) { @@ -923,8 +969,14 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da defaultWebBrowser = presentationData.strings.WebBrowser_Telegram } + let previousSensitiveContent = sensitiveContent.swap(contentSettingsConfiguration?.sensitiveContentEnabled) + var animateChanges = false + if previousSensitiveContent != contentSettingsConfiguration?.sensitiveContentEnabled { + animateChanges = true + } + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData, defaultWebBrowser: defaultWebBrowser, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData, defaultWebBrowser: defaultWebBrowser, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers, mediaSettings: mediaSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 67ddd69b044..aa3aa8cd7ce 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -26,7 +26,7 @@ private func shareLink(for server: ProxyServerSettings) -> String { return link } -private final class proxyServerSettingsControllerArguments { +private final class ProxyServerSettingsControllerArguments { let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void let share: () -> Void let usePasteboardSettings: () -> Void @@ -113,7 +113,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! proxyServerSettingsControllerArguments + let arguments = arguments as! ProxyServerSettingsControllerArguments switch self { case let .usePasteboardSettings(_, title): return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { @@ -158,7 +158,7 @@ private enum ProxySettingsEntry: ItemListNodeEntry { case let .credentialsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .credentialsUsername(_, _, placeholder, text): - return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in + return ItemListSingleLineInputItem(context: nil, presentationData: presentationData, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.username = value @@ -306,7 +306,7 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: var shareImpl: (() -> Void)? - let arguments = proxyServerSettingsControllerArguments(updateState: { f in + let arguments = ProxyServerSettingsControllerArguments(updateState: { f in updateState(f) }, share: { shareImpl?() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index 67bb0ba0e32..e085ebf7e6e 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -18,12 +18,14 @@ private enum CreatePasswordField { } private final class CreatePasswordControllerArguments { + let context: AccountContext let updateFieldText: (CreatePasswordField, String) -> Void let selectNextInputItem: (CreatePasswordEntryTag) -> Void let save: () -> Void let cancelEmailConfirmation: () -> Void - init(updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) { + init(context: AccountContext, updateFieldText: @escaping (CreatePasswordField, String) -> Void, selectNextInputItem: @escaping (CreatePasswordEntryTag) -> Void, save: @escaping () -> Void, cancelEmailConfirmation: @escaping () -> Void) { + self.context = context self.updateFieldText = updateFieldText self.selectNextInputItem = selectNextInputItem self.save = save @@ -321,7 +323,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr } } - let arguments = CreatePasswordControllerArguments(updateFieldText: { field, updatedText in + let arguments = CreatePasswordControllerArguments(context: context, updateFieldText: { field, updatedText in updateState { state in var state = state switch field { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index ac7aa8d89ef..f8efe84b9d0 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -16,6 +16,7 @@ import PasswordSetupUI import Markdown private final class TwoStepVerificationUnlockSettingsControllerArguments { + let context: AccountContext let updatePasswordText: (String) -> Void let checkPassword: () -> Void let openForgotPassword: () -> Void @@ -28,7 +29,8 @@ private final class TwoStepVerificationUnlockSettingsControllerArguments { let declinePasswordReset: () -> Void let resetPassword: () -> Void - init(updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void, declinePasswordReset: @escaping () -> Void, resetPassword: @escaping () -> Void) { + init(context: AccountContext, updatePasswordText: @escaping (String) -> Void, checkPassword: @escaping () -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void, updateEmailCode: @escaping (String) -> Void, openConfirmEmail: @escaping () -> Void, declinePasswordReset: @escaping () -> Void, resetPassword: @escaping () -> Void) { + self.context = context self.updatePasswordText = updatePasswordText self.checkPassword = checkPassword self.openForgotPassword = openForgotPassword @@ -423,7 +425,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, }) } - let arguments = TwoStepVerificationUnlockSettingsControllerArguments(updatePasswordText: { updatedText in + let arguments = TwoStepVerificationUnlockSettingsControllerArguments(context: context, updatePasswordText: { updatedText in updateState { state in var state = state state.passwordText = updatedText diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 94e9ed572db..347f115d99e 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -227,6 +227,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -315,10 +316,10 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let timestamp = self.referenceTimestamp diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 336cf3e8b6f..974bf7976fc 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -376,6 +376,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -464,10 +465,10 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) - let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer7: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(6)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 13650ccf002..ca1656d854d 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1024,7 +1024,7 @@ private enum StatsEntry: ItemListNodeEntry { case let .boostOverview(_, stats, isGroup): return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: isGroup, stats: stats, sectionId: self.section, style: .blocks) case let .boostLink(_, link): - let invite: ExportedInvitation = .link(link: link, title: nil, isPermanent: false, requestApproval: false, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil) + let invite: ExportedInvitation = .link(link: link, title: nil, isPermanent: false, requestApproval: false, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil, requestedCount: nil, pricing: nil) return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { arguments.copyBoostLink(link) }, shareAction: { diff --git a/submodules/StatisticsUI/Sources/GroupStatsController.swift b/submodules/StatisticsUI/Sources/GroupStatsController.swift index c56e2c81ff9..324a0a5eaa3 100644 --- a/submodules/StatisticsUI/Sources/GroupStatsController.swift +++ b/submodules/StatisticsUI/Sources/GroupStatsController.swift @@ -686,7 +686,7 @@ private func canEditAdminRights(accountPeerId: EnginePeer.Id, channelPeer: Engin switch initialParticipant { case .creator: return false - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId } else { @@ -890,7 +890,7 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat let _ = (context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in - let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: participantPeerId) + let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: participantPeerId, starsState: nil) navigationController.pushViewController(controller) }) } diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 6a225c18dcd..1c8ed37359a 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -231,15 +231,21 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { var itemDate: String switch item.transaction.peer { case let .peer(peer): - if !item.transaction.media.isEmpty { + if !item.transaction.media.isEmpty { itemTitle = item.presentationData.strings.Stars_Intro_Transaction_MediaPurchase itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) } else if let title = item.transaction.title { itemTitle = title itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) } else { + if item.transaction.flags.contains(.isReaction) { + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_Reaction_Title + } else if let _ = item.transaction.subscriptionPeriod { + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_SubscriptionFee_Title + } else { + itemSubtitle = nil + } itemTitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) - itemSubtitle = nil } case .appStore: itemTitle = item.presentationData.strings.Stars_Intro_Transaction_AppleTopUp_Title @@ -272,9 +278,15 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { } itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? item.presentationData.theme.list.itemDestructiveColor : item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor) + var itemDateColor = item.presentationData.theme.list.itemSecondaryTextColor itemDate = stringForMediumCompactDate(timestamp: item.transaction.date, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) if item.transaction.flags.contains(.isRefund) { itemDate += " – \(item.presentationData.strings.Stars_Intro_Transaction_Refund)" + } else if item.transaction.flags.contains(.isPending) { + itemDate += " – \(item.presentationData.strings.Monetization_Transaction_Pending)" + } else if item.transaction.flags.contains(.isFailed) { + itemDate += " – \(item.presentationData.strings.Monetization_Transaction_Failed)" + itemDateColor = item.presentationData.theme.list.itemDestructiveColor } var titleComponents: [AnyComponentWithIdentity] = [] @@ -305,7 +317,7 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { text: .plain(NSAttributedString( string: itemDate, font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), - textColor: item.presentationData.theme.list.itemSecondaryTextColor + textColor: itemDateColor )), maximumNumberOfLines: 1 ))) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 11b0f8515c1..0eeb02d587e 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -92,7 +92,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1071145937] = { return Api.BotCommandScope.parse_botCommandScopePeerAdmins($0) } dict[169026035] = { return Api.BotCommandScope.parse_botCommandScopePeerUser($0) } dict[1011811544] = { return Api.BotCommandScope.parse_botCommandScopeUsers($0) } - dict[-1892676777] = { return Api.BotInfo.parse_botInfo($0) } + dict[-2109505932] = { return Api.BotInfo.parse_botInfo($0) } dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) } dict[416402882] = { return Api.BotInlineMessage.parse_botInlineMessageMediaContact($0) } dict[85477117] = { return Api.BotInlineMessage.parse_botInlineMessageMediaGeo($0) } @@ -170,6 +170,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[460916654] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleInvites($0) } dict[-886388890] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleNoForwards($0) } dict[1599903217] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionTogglePreHistoryHidden($0) } + dict[1621597305] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleSignatureProfiles($0) } dict[648939889] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleSignatures($0) } dict[1401984889] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleSlowMode($0) } dict[-370660328] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionUpdatePinned($0) } @@ -178,12 +179,12 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1078612597] = { return Api.ChannelLocation.parse_channelLocationEmpty($0) } dict[-847783593] = { return Api.ChannelMessagesFilter.parse_channelMessagesFilter($0) } dict[-1798033689] = { return Api.ChannelMessagesFilter.parse_channelMessagesFilterEmpty($0) } - dict[-1072953408] = { return Api.ChannelParticipant.parse_channelParticipant($0) } + dict[-885426663] = { return Api.ChannelParticipant.parse_channelParticipant($0) } dict[885242707] = { return Api.ChannelParticipant.parse_channelParticipantAdmin($0) } dict[1844969806] = { return Api.ChannelParticipant.parse_channelParticipantBanned($0) } dict[803602899] = { return Api.ChannelParticipant.parse_channelParticipantCreator($0) } dict[453242886] = { return Api.ChannelParticipant.parse_channelParticipantLeft($0) } - dict[900251559] = { return Api.ChannelParticipant.parse_channelParticipantSelf($0) } + dict[1331723247] = { return Api.ChannelParticipant.parse_channelParticipantSelf($0) } dict[-1268741783] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsAdmins($0) } dict[338142689] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsBanned($0) } dict[-1328445861] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsBots($0) } @@ -192,7 +193,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-531931925] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsMentions($0) } dict[-566281095] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsRecent($0) } dict[106343499] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsSearch($0) } - dict[179174543] = { return Api.Chat.parse_channel($0) } + dict[-29067075] = { return Api.Chat.parse_channel($0) } dict[399807445] = { return Api.Chat.parse_channelForbidden($0) } dict[1103884886] = { return Api.Chat.parse_chat($0) } dict[693512293] = { return Api.Chat.parse_chatEmpty($0) } @@ -202,7 +203,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1626209256] = { return Api.ChatBannedRights.parse_chatBannedRights($0) } dict[-1146407795] = { return Api.ChatFull.parse_channelFull($0) } dict[640893467] = { return Api.ChatFull.parse_chatFull($0) } - dict[-840897472] = { return Api.ChatInvite.parse_chatInvite($0) } + dict[-26920803] = { return Api.ChatInvite.parse_chatInvite($0) } dict[1516793212] = { return Api.ChatInvite.parse_chatInviteAlready($0) } dict[1634294960] = { return Api.ChatInvite.parse_chatInvitePeek($0) } dict[-1940201511] = { return Api.ChatInviteImporter.parse_chatInviteImporter($0) } @@ -274,7 +275,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1038136962] = { return Api.EncryptedFile.parse_encryptedFileEmpty($0) } dict[-317144808] = { return Api.EncryptedMessage.parse_encryptedMessage($0) } dict[594758406] = { return Api.EncryptedMessage.parse_encryptedMessageService($0) } - dict[179611673] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) } + dict[-1574126186] = { return Api.ExportedChatInvite.parse_chatInviteExported($0) } dict[-317687113] = { return Api.ExportedChatInvite.parse_chatInvitePublicJoinRequests($0) } dict[206668204] = { return Api.ExportedChatlistInvite.parse_exportedChatlistInvite($0) } dict[1103040667] = { return Api.ExportedContactToken.parse_exportedContactToken($0) } @@ -372,6 +373,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1210199983] = { return Api.InputGeoPoint.parse_inputGeoPoint($0) } dict[-457104426] = { return Api.InputGeoPoint.parse_inputGeoPointEmpty($0) } dict[-659913713] = { return Api.InputGroupCall.parse_inputGroupCall($0) } + dict[887591921] = { return Api.InputInvoice.parse_inputInvoiceChatInviteSubscription($0) } dict[-977967015] = { return Api.InputInvoice.parse_inputInvoiceMessage($0) } dict[-1734841331] = { return Api.InputInvoice.parse_inputInvoicePremiumGiftCode($0) } dict[-1020867857] = { return Api.InputInvoice.parse_inputInvoiceSlug($0) } @@ -618,7 +620,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1959634180] = { return Api.MessagePeerVote.parse_messagePeerVoteInputOption($0) } dict[1177089766] = { return Api.MessagePeerVote.parse_messagePeerVoteMultiple($0) } dict[182649427] = { return Api.MessageRange.parse_messageRange($0) } - dict[1328256121] = { return Api.MessageReactions.parse_messageReactions($0) } + dict[171155211] = { return Api.MessageReactions.parse_messageReactions($0) } + dict[1269016922] = { return Api.MessageReactor.parse_messageReactor($0) } dict[-2083123262] = { return Api.MessageReplies.parse_messageReplies($0) } dict[-1346631205] = { return Api.MessageReplyHeader.parse_messageReplyHeader($0) } dict[240843065] = { return Api.MessageReplyHeader.parse_messageReplyStoryHeader($0) } @@ -767,6 +770,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1992950669] = { return Api.Reaction.parse_reactionCustomEmoji($0) } dict[455247544] = { return Api.Reaction.parse_reactionEmoji($0) } dict[2046153753] = { return Api.Reaction.parse_reactionEmpty($0) } + dict[1379771627] = { return Api.Reaction.parse_reactionPaid($0) } dict[-1546531968] = { return Api.ReactionCount.parse_reactionCount($0) } dict[1268654752] = { return Api.ReactionNotificationsFrom.parse_reactionNotificationsFromAll($0) } dict[-1161583078] = { return Api.ReactionNotificationsFrom.parse_reactionNotificationsFromContacts($0) } @@ -877,12 +881,14 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-651419003] = { return Api.SendMessageAction.parse_speakingInGroupCallAction($0) } dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) } dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } - dict[-1108478618] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } + dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[1577421297] = { return Api.StarsGiftOption.parse_starsGiftOption($0) } dict[2033461574] = { return Api.StarsRevenueStatus.parse_starsRevenueStatus($0) } + dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) } + dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } - dict[766853519] = { return Api.StarsTransaction.parse_starsTransaction($0) } + dict[1127934763] = { return Api.StarsTransaction.parse_starsTransaction($0) } dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) } dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) } dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } @@ -1327,7 +1333,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[961445665] = { return Api.payments.StarsRevenueAdsAccountUrl.parse_starsRevenueAdsAccountUrl($0) } dict[-919881925] = { return Api.payments.StarsRevenueStats.parse_starsRevenueStats($0) } dict[497778871] = { return Api.payments.StarsRevenueWithdrawalUrl.parse_starsRevenueWithdrawalUrl($0) } - dict[-1930105248] = { return Api.payments.StarsStatus.parse_starsStatus($0) } + dict[-1141231252] = { return Api.payments.StarsStatus.parse_starsStatus($0) } dict[-784000893] = { return Api.payments.ValidatedRequestedInfo.parse_validatedRequestedInfo($0) } dict[541839704] = { return Api.phone.ExportedGroupCallInvite.parse_exportedGroupCallInvite($0) } dict[-1636664659] = { return Api.phone.GroupCall.parse_groupCall($0) } @@ -1824,6 +1830,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.MessageReactions: _1.serialize(buffer, boxed) + case let _1 as Api.MessageReactor: + _1.serialize(buffer, boxed) case let _1 as Api.MessageReplies: _1.serialize(buffer, boxed) case let _1 as Api.MessageReplyHeader: @@ -2000,6 +2008,10 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.StarsRevenueStatus: _1.serialize(buffer, boxed) + case let _1 as Api.StarsSubscription: + _1.serialize(buffer, boxed) + case let _1 as Api.StarsSubscriptionPricing: + _1.serialize(buffer, boxed) case let _1 as Api.StarsTopupOption: _1.serialize(buffer, boxed) case let _1 as Api.StarsTransaction: diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 79acdb0d0a6..4b7744a8018 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -1,5 +1,6 @@ public extension Api { indirect enum InputInvoice: TypeConstructorDescription { + case inputInvoiceChatInviteSubscription(hash: String) case inputInvoiceMessage(peer: Api.InputPeer, msgId: Int32) case inputInvoicePremiumGiftCode(purpose: Api.InputStorePaymentPurpose, option: Api.PremiumGiftCodeOption) case inputInvoiceSlug(slug: String) @@ -7,6 +8,12 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .inputInvoiceChatInviteSubscription(let hash): + if boxed { + buffer.appendInt32(887591921) + } + serializeString(hash, buffer: buffer, boxed: false) + break case .inputInvoiceMessage(let peer, let msgId): if boxed { buffer.appendInt32(-977967015) @@ -38,6 +45,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .inputInvoiceChatInviteSubscription(let hash): + return ("inputInvoiceChatInviteSubscription", [("hash", hash as Any)]) case .inputInvoiceMessage(let peer, let msgId): return ("inputInvoiceMessage", [("peer", peer as Any), ("msgId", msgId as Any)]) case .inputInvoicePremiumGiftCode(let purpose, let option): @@ -49,6 +58,17 @@ public extension Api { } } + public static func parse_inputInvoiceChatInviteSubscription(_ reader: BufferReader) -> InputInvoice? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputInvoice.inputInvoiceChatInviteSubscription(hash: _1!) + } + else { + return nil + } + } public static func parse_inputInvoiceMessage(_ reader: BufferReader) -> InputInvoice? { var _1: Api.InputPeer? if let signature = reader.readInt32() { @@ -920,113 +940,3 @@ public extension Api { } } -public extension Api { - enum InputPaymentCredentials: TypeConstructorDescription { - case inputPaymentCredentials(flags: Int32, data: Api.DataJSON) - case inputPaymentCredentialsApplePay(paymentData: Api.DataJSON) - case inputPaymentCredentialsGooglePay(paymentToken: Api.DataJSON) - case inputPaymentCredentialsSaved(id: String, tmpPassword: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputPaymentCredentials(let flags, let data): - if boxed { - buffer.appendInt32(873977640) - } - serializeInt32(flags, buffer: buffer, boxed: false) - data.serialize(buffer, true) - break - case .inputPaymentCredentialsApplePay(let paymentData): - if boxed { - buffer.appendInt32(178373535) - } - paymentData.serialize(buffer, true) - break - case .inputPaymentCredentialsGooglePay(let paymentToken): - if boxed { - buffer.appendInt32(-1966921727) - } - paymentToken.serialize(buffer, true) - break - case .inputPaymentCredentialsSaved(let id, let tmpPassword): - if boxed { - buffer.appendInt32(-1056001329) - } - serializeString(id, buffer: buffer, boxed: false) - serializeBytes(tmpPassword, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputPaymentCredentials(let flags, let data): - return ("inputPaymentCredentials", [("flags", flags as Any), ("data", data as Any)]) - case .inputPaymentCredentialsApplePay(let paymentData): - return ("inputPaymentCredentialsApplePay", [("paymentData", paymentData as Any)]) - case .inputPaymentCredentialsGooglePay(let paymentToken): - return ("inputPaymentCredentialsGooglePay", [("paymentToken", paymentToken as Any)]) - case .inputPaymentCredentialsSaved(let id, let tmpPassword): - return ("inputPaymentCredentialsSaved", [("id", id as Any), ("tmpPassword", tmpPassword as Any)]) - } - } - - public static func parse_inputPaymentCredentials(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.DataJSON? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPaymentCredentials.inputPaymentCredentials(flags: _1!, data: _2!) - } - else { - return nil - } - } - public static func parse_inputPaymentCredentialsApplePay(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: Api.DataJSON? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPaymentCredentials.inputPaymentCredentialsApplePay(paymentData: _1!) - } - else { - return nil - } - } - public static func parse_inputPaymentCredentialsGooglePay(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: Api.DataJSON? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - let _c1 = _1 != nil - if _c1 { - return Api.InputPaymentCredentials.inputPaymentCredentialsGooglePay(paymentToken: _1!) - } - else { - return nil - } - } - public static func parse_inputPaymentCredentialsSaved(_ reader: BufferReader) -> InputPaymentCredentials? { - var _1: String? - _1 = parseString(reader) - var _2: Buffer? - _2 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputPaymentCredentials.inputPaymentCredentialsSaved(id: _1!, tmpPassword: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 8845ce3ec63..7eb2f373261 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -1,3 +1,113 @@ +public extension Api { + enum InputPaymentCredentials: TypeConstructorDescription { + case inputPaymentCredentials(flags: Int32, data: Api.DataJSON) + case inputPaymentCredentialsApplePay(paymentData: Api.DataJSON) + case inputPaymentCredentialsGooglePay(paymentToken: Api.DataJSON) + case inputPaymentCredentialsSaved(id: String, tmpPassword: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputPaymentCredentials(let flags, let data): + if boxed { + buffer.appendInt32(873977640) + } + serializeInt32(flags, buffer: buffer, boxed: false) + data.serialize(buffer, true) + break + case .inputPaymentCredentialsApplePay(let paymentData): + if boxed { + buffer.appendInt32(178373535) + } + paymentData.serialize(buffer, true) + break + case .inputPaymentCredentialsGooglePay(let paymentToken): + if boxed { + buffer.appendInt32(-1966921727) + } + paymentToken.serialize(buffer, true) + break + case .inputPaymentCredentialsSaved(let id, let tmpPassword): + if boxed { + buffer.appendInt32(-1056001329) + } + serializeString(id, buffer: buffer, boxed: false) + serializeBytes(tmpPassword, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputPaymentCredentials(let flags, let data): + return ("inputPaymentCredentials", [("flags", flags as Any), ("data", data as Any)]) + case .inputPaymentCredentialsApplePay(let paymentData): + return ("inputPaymentCredentialsApplePay", [("paymentData", paymentData as Any)]) + case .inputPaymentCredentialsGooglePay(let paymentToken): + return ("inputPaymentCredentialsGooglePay", [("paymentToken", paymentToken as Any)]) + case .inputPaymentCredentialsSaved(let id, let tmpPassword): + return ("inputPaymentCredentialsSaved", [("id", id as Any), ("tmpPassword", tmpPassword as Any)]) + } + } + + public static func parse_inputPaymentCredentials(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.DataJSON? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPaymentCredentials.inputPaymentCredentials(flags: _1!, data: _2!) + } + else { + return nil + } + } + public static func parse_inputPaymentCredentialsApplePay(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: Api.DataJSON? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPaymentCredentials.inputPaymentCredentialsApplePay(paymentData: _1!) + } + else { + return nil + } + } + public static func parse_inputPaymentCredentialsGooglePay(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: Api.DataJSON? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + let _c1 = _1 != nil + if _c1 { + return Api.InputPaymentCredentials.inputPaymentCredentialsGooglePay(paymentToken: _1!) + } + else { + return nil + } + } + public static func parse_inputPaymentCredentialsSaved(_ reader: BufferReader) -> InputPaymentCredentials? { + var _1: String? + _1 = parseString(reader) + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputPaymentCredentials.inputPaymentCredentialsSaved(id: _1!, tmpPassword: _2!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputPeer: TypeConstructorDescription { case inputPeerChannel(channelId: Int64, accessHash: Int64) diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 85ed7f414c4..4652a76a82d 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -200,13 +200,13 @@ public extension Api { } public extension Api { enum MessageReactions: TypeConstructorDescription { - case messageReactions(flags: Int32, results: [Api.ReactionCount], recentReactions: [Api.MessagePeerReaction]?) + case messageReactions(flags: Int32, results: [Api.ReactionCount], recentReactions: [Api.MessagePeerReaction]?, topReactors: [Api.MessageReactor]?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .messageReactions(let flags, let results, let recentReactions): + case .messageReactions(let flags, let results, let recentReactions, let topReactors): if boxed { - buffer.appendInt32(1328256121) + buffer.appendInt32(171155211) } serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -219,14 +219,19 @@ public extension Api { for item in recentReactions! { item.serialize(buffer, true) }} + if Int(flags) & Int(1 << 4) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(topReactors!.count)) + for item in topReactors! { + item.serialize(buffer, true) + }} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .messageReactions(let flags, let results, let recentReactions): - return ("messageReactions", [("flags", flags as Any), ("results", results as Any), ("recentReactions", recentReactions as Any)]) + case .messageReactions(let flags, let results, let recentReactions, let topReactors): + return ("messageReactions", [("flags", flags as Any), ("results", results as Any), ("recentReactions", recentReactions as Any), ("topReactors", topReactors as Any)]) } } @@ -241,11 +246,62 @@ public extension Api { if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessagePeerReaction.self) } } + var _4: [Api.MessageReactor]? + if Int(_1!) & Int(1 << 4) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageReactor.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MessageReactions.messageReactions(flags: _1!, results: _2!, recentReactions: _3, topReactors: _4) + } + else { + return nil + } + } + + } +} +public extension Api { + enum MessageReactor: TypeConstructorDescription { + case messageReactor(flags: Int32, peerId: Api.Peer?, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageReactor(let flags, let peerId, let count): + if boxed { + buffer.appendInt32(1269016922) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {peerId!.serialize(buffer, true)} + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .messageReactor(let flags, let peerId, let count): + return ("messageReactor", [("flags", flags as Any), ("peerId", peerId as Any), ("count", count as Any)]) + } + } + + public static func parse_messageReactor(_ reader: BufferReader) -> MessageReactor? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + let _c3 = _3 != nil if _c1 && _c2 && _c3 { - return Api.MessageReactions.messageReactions(flags: _1!, results: _2!, recentReactions: _3) + return Api.MessageReactor.messageReactor(flags: _1!, peerId: _2, count: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index 8855da4143b..f7891e99446 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -136,13 +136,13 @@ public extension Api { } public extension Api { enum BotInfo: TypeConstructorDescription { - case botInfo(flags: Int32, userId: Int64?, description: String?, descriptionPhoto: Api.Photo?, descriptionDocument: Api.Document?, commands: [Api.BotCommand]?, menuButton: Api.BotMenuButton?) + case botInfo(flags: Int32, userId: Int64?, description: String?, descriptionPhoto: Api.Photo?, descriptionDocument: Api.Document?, commands: [Api.BotCommand]?, menuButton: Api.BotMenuButton?, privacyPolicyUrl: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton): + case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton, let privacyPolicyUrl): if boxed { - buffer.appendInt32(-1892676777) + buffer.appendInt32(-2109505932) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(userId!, buffer: buffer, boxed: false)} @@ -155,14 +155,15 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 3) != 0 {menuButton!.serialize(buffer, true)} + if Int(flags) & Int(1 << 7) != 0 {serializeString(privacyPolicyUrl!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton): - return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any)]) + case .botInfo(let flags, let userId, let description, let descriptionPhoto, let descriptionDocument, let commands, let menuButton, let privacyPolicyUrl): + return ("botInfo", [("flags", flags as Any), ("userId", userId as Any), ("description", description as Any), ("descriptionPhoto", descriptionPhoto as Any), ("descriptionDocument", descriptionDocument as Any), ("commands", commands as Any), ("menuButton", menuButton as Any), ("privacyPolicyUrl", privacyPolicyUrl as Any)]) } } @@ -189,6 +190,8 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.BotMenuButton } } + var _8: String? + if Int(_1!) & Int(1 << 7) != 0 {_8 = parseString(reader) } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil @@ -196,8 +199,9 @@ public extension Api { let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.BotInfo.botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7) + let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.BotInfo.botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7, privacyPolicyUrl: _8) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index 2b1d7e1f79b..206cb533bb7 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -309,6 +309,7 @@ public extension Api { case reactionCustomEmoji(documentId: Int64) case reactionEmoji(emoticon: String) case reactionEmpty + case reactionPaid public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -329,6 +330,12 @@ public extension Api { buffer.appendInt32(2046153753) } + break + case .reactionPaid: + if boxed { + buffer.appendInt32(1379771627) + } + break } } @@ -341,6 +348,8 @@ public extension Api { return ("reactionEmoji", [("emoticon", emoticon as Any)]) case .reactionEmpty: return ("reactionEmpty", []) + case .reactionPaid: + return ("reactionPaid", []) } } @@ -369,6 +378,9 @@ public extension Api { public static func parse_reactionEmpty(_ reader: BufferReader) -> Reaction? { return Api.Reaction.reactionEmpty } + public static func parse_reactionPaid(_ reader: BufferReader) -> Reaction? { + return Api.Reaction.reactionPaid + } } } @@ -858,139 +870,3 @@ public extension Api { } } -public extension Api { - enum ReportReason: TypeConstructorDescription { - case inputReportReasonChildAbuse - case inputReportReasonCopyright - case inputReportReasonFake - case inputReportReasonGeoIrrelevant - case inputReportReasonIllegalDrugs - case inputReportReasonOther - case inputReportReasonPersonalDetails - case inputReportReasonPornography - case inputReportReasonSpam - case inputReportReasonViolence - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputReportReasonChildAbuse: - if boxed { - buffer.appendInt32(-1376497949) - } - - break - case .inputReportReasonCopyright: - if boxed { - buffer.appendInt32(-1685456582) - } - - break - case .inputReportReasonFake: - if boxed { - buffer.appendInt32(-170010905) - } - - break - case .inputReportReasonGeoIrrelevant: - if boxed { - buffer.appendInt32(-606798099) - } - - break - case .inputReportReasonIllegalDrugs: - if boxed { - buffer.appendInt32(177124030) - } - - break - case .inputReportReasonOther: - if boxed { - buffer.appendInt32(-1041980751) - } - - break - case .inputReportReasonPersonalDetails: - if boxed { - buffer.appendInt32(-1631091139) - } - - break - case .inputReportReasonPornography: - if boxed { - buffer.appendInt32(777640226) - } - - break - case .inputReportReasonSpam: - if boxed { - buffer.appendInt32(1490799288) - } - - break - case .inputReportReasonViolence: - if boxed { - buffer.appendInt32(505595789) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputReportReasonChildAbuse: - return ("inputReportReasonChildAbuse", []) - case .inputReportReasonCopyright: - return ("inputReportReasonCopyright", []) - case .inputReportReasonFake: - return ("inputReportReasonFake", []) - case .inputReportReasonGeoIrrelevant: - return ("inputReportReasonGeoIrrelevant", []) - case .inputReportReasonIllegalDrugs: - return ("inputReportReasonIllegalDrugs", []) - case .inputReportReasonOther: - return ("inputReportReasonOther", []) - case .inputReportReasonPersonalDetails: - return ("inputReportReasonPersonalDetails", []) - case .inputReportReasonPornography: - return ("inputReportReasonPornography", []) - case .inputReportReasonSpam: - return ("inputReportReasonSpam", []) - case .inputReportReasonViolence: - return ("inputReportReasonViolence", []) - } - } - - public static func parse_inputReportReasonChildAbuse(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonChildAbuse - } - public static func parse_inputReportReasonCopyright(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonCopyright - } - public static func parse_inputReportReasonFake(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonFake - } - public static func parse_inputReportReasonGeoIrrelevant(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonGeoIrrelevant - } - public static func parse_inputReportReasonIllegalDrugs(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonIllegalDrugs - } - public static func parse_inputReportReasonOther(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonOther - } - public static func parse_inputReportReasonPersonalDetails(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonPersonalDetails - } - public static func parse_inputReportReasonPornography(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonPornography - } - public static func parse_inputReportReasonSpam(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonSpam - } - public static func parse_inputReportReasonViolence(_ reader: BufferReader) -> ReportReason? { - return Api.ReportReason.inputReportReasonViolence - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index 83924b0d6bb..73eedd5544f 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -1,3 +1,139 @@ +public extension Api { + enum ReportReason: TypeConstructorDescription { + case inputReportReasonChildAbuse + case inputReportReasonCopyright + case inputReportReasonFake + case inputReportReasonGeoIrrelevant + case inputReportReasonIllegalDrugs + case inputReportReasonOther + case inputReportReasonPersonalDetails + case inputReportReasonPornography + case inputReportReasonSpam + case inputReportReasonViolence + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputReportReasonChildAbuse: + if boxed { + buffer.appendInt32(-1376497949) + } + + break + case .inputReportReasonCopyright: + if boxed { + buffer.appendInt32(-1685456582) + } + + break + case .inputReportReasonFake: + if boxed { + buffer.appendInt32(-170010905) + } + + break + case .inputReportReasonGeoIrrelevant: + if boxed { + buffer.appendInt32(-606798099) + } + + break + case .inputReportReasonIllegalDrugs: + if boxed { + buffer.appendInt32(177124030) + } + + break + case .inputReportReasonOther: + if boxed { + buffer.appendInt32(-1041980751) + } + + break + case .inputReportReasonPersonalDetails: + if boxed { + buffer.appendInt32(-1631091139) + } + + break + case .inputReportReasonPornography: + if boxed { + buffer.appendInt32(777640226) + } + + break + case .inputReportReasonSpam: + if boxed { + buffer.appendInt32(1490799288) + } + + break + case .inputReportReasonViolence: + if boxed { + buffer.appendInt32(505595789) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputReportReasonChildAbuse: + return ("inputReportReasonChildAbuse", []) + case .inputReportReasonCopyright: + return ("inputReportReasonCopyright", []) + case .inputReportReasonFake: + return ("inputReportReasonFake", []) + case .inputReportReasonGeoIrrelevant: + return ("inputReportReasonGeoIrrelevant", []) + case .inputReportReasonIllegalDrugs: + return ("inputReportReasonIllegalDrugs", []) + case .inputReportReasonOther: + return ("inputReportReasonOther", []) + case .inputReportReasonPersonalDetails: + return ("inputReportReasonPersonalDetails", []) + case .inputReportReasonPornography: + return ("inputReportReasonPornography", []) + case .inputReportReasonSpam: + return ("inputReportReasonSpam", []) + case .inputReportReasonViolence: + return ("inputReportReasonViolence", []) + } + } + + public static func parse_inputReportReasonChildAbuse(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonChildAbuse + } + public static func parse_inputReportReasonCopyright(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonCopyright + } + public static func parse_inputReportReasonFake(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonFake + } + public static func parse_inputReportReasonGeoIrrelevant(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonGeoIrrelevant + } + public static func parse_inputReportReasonIllegalDrugs(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonIllegalDrugs + } + public static func parse_inputReportReasonOther(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonOther + } + public static func parse_inputReportReasonPersonalDetails(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonPersonalDetails + } + public static func parse_inputReportReasonPornography(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonPornography + } + public static func parse_inputReportReasonSpam(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonSpam + } + public static func parse_inputReportReasonViolence(_ reader: BufferReader) -> ReportReason? { + return Api.ReportReason.inputReportReasonViolence + } + + } +} public extension Api { enum RequestPeerType: TypeConstructorDescription { case requestPeerTypeBroadcast(flags: Int32, hasUsername: Api.Bool?, userAdminRights: Api.ChatAdminRights?, botAdminRights: Api.ChatAdminRights?) @@ -688,399 +824,3 @@ public extension Api { } } -public extension Api { - enum SavedContact: TypeConstructorDescription { - case savedPhoneContact(phone: String, firstName: String, lastName: String, date: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .savedPhoneContact(let phone, let firstName, let lastName, let date): - if boxed { - buffer.appendInt32(289586518) - } - serializeString(phone, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .savedPhoneContact(let phone, let firstName, let lastName, let date): - return ("savedPhoneContact", [("phone", phone as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("date", date as Any)]) - } - } - - public static func parse_savedPhoneContact(_ reader: BufferReader) -> SavedContact? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.SavedContact.savedPhoneContact(phone: _1!, firstName: _2!, lastName: _3!, date: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum SavedDialog: TypeConstructorDescription { - case savedDialog(flags: Int32, peer: Api.Peer, topMessage: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .savedDialog(let flags, let peer, let topMessage): - if boxed { - buffer.appendInt32(-1115174036) - } - serializeInt32(flags, buffer: buffer, boxed: false) - peer.serialize(buffer, true) - serializeInt32(topMessage, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .savedDialog(let flags, let peer, let topMessage): - return ("savedDialog", [("flags", flags as Any), ("peer", peer as Any), ("topMessage", topMessage as Any)]) - } - } - - public static func parse_savedDialog(_ reader: BufferReader) -> SavedDialog? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.Peer? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.SavedDialog.savedDialog(flags: _1!, peer: _2!, topMessage: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum SavedReactionTag: TypeConstructorDescription { - case savedReactionTag(flags: Int32, reaction: Api.Reaction, title: String?, count: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .savedReactionTag(let flags, let reaction, let title, let count): - if boxed { - buffer.appendInt32(-881854424) - } - serializeInt32(flags, buffer: buffer, boxed: false) - reaction.serialize(buffer, true) - if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - serializeInt32(count, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .savedReactionTag(let flags, let reaction, let title, let count): - return ("savedReactionTag", [("flags", flags as Any), ("reaction", reaction as Any), ("title", title as Any), ("count", count as Any)]) - } - } - - public static func parse_savedReactionTag(_ reader: BufferReader) -> SavedReactionTag? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.Reaction? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Reaction - } - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.SavedReactionTag.savedReactionTag(flags: _1!, reaction: _2!, title: _3, count: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum SearchResultsCalendarPeriod: TypeConstructorDescription { - case searchResultsCalendarPeriod(date: Int32, minMsgId: Int32, maxMsgId: Int32, count: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .searchResultsCalendarPeriod(let date, let minMsgId, let maxMsgId, let count): - if boxed { - buffer.appendInt32(-911191137) - } - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(minMsgId, buffer: buffer, boxed: false) - serializeInt32(maxMsgId, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .searchResultsCalendarPeriod(let date, let minMsgId, let maxMsgId, let count): - return ("searchResultsCalendarPeriod", [("date", date as Any), ("minMsgId", minMsgId as Any), ("maxMsgId", maxMsgId as Any), ("count", count as Any)]) - } - } - - public static func parse_searchResultsCalendarPeriod(_ reader: BufferReader) -> SearchResultsCalendarPeriod? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.SearchResultsCalendarPeriod.searchResultsCalendarPeriod(date: _1!, minMsgId: _2!, maxMsgId: _3!, count: _4!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum SearchResultsPosition: TypeConstructorDescription { - case searchResultPosition(msgId: Int32, date: Int32, offset: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .searchResultPosition(let msgId, let date, let offset): - if boxed { - buffer.appendInt32(2137295719) - } - serializeInt32(msgId, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(offset, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .searchResultPosition(let msgId, let date, let offset): - return ("searchResultPosition", [("msgId", msgId as Any), ("date", date as Any), ("offset", offset as Any)]) - } - } - - public static func parse_searchResultPosition(_ reader: BufferReader) -> SearchResultsPosition? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.SearchResultsPosition.searchResultPosition(msgId: _1!, date: _2!, offset: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum SecureCredentialsEncrypted: TypeConstructorDescription { - case secureCredentialsEncrypted(data: Buffer, hash: Buffer, secret: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .secureCredentialsEncrypted(let data, let hash, let secret): - if boxed { - buffer.appendInt32(871426631) - } - serializeBytes(data, buffer: buffer, boxed: false) - serializeBytes(hash, buffer: buffer, boxed: false) - serializeBytes(secret, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .secureCredentialsEncrypted(let data, let hash, let secret): - return ("secureCredentialsEncrypted", [("data", data as Any), ("hash", hash as Any), ("secret", secret as Any)]) - } - } - - public static func parse_secureCredentialsEncrypted(_ reader: BufferReader) -> SecureCredentialsEncrypted? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Buffer? - _2 = parseBytes(reader) - var _3: Buffer? - _3 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.SecureCredentialsEncrypted.secureCredentialsEncrypted(data: _1!, hash: _2!, secret: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum SecureData: TypeConstructorDescription { - case secureData(data: Buffer, dataHash: Buffer, secret: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .secureData(let data, let dataHash, let secret): - if boxed { - buffer.appendInt32(-1964327229) - } - serializeBytes(data, buffer: buffer, boxed: false) - serializeBytes(dataHash, buffer: buffer, boxed: false) - serializeBytes(secret, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .secureData(let data, let dataHash, let secret): - return ("secureData", [("data", data as Any), ("dataHash", dataHash as Any), ("secret", secret as Any)]) - } - } - - public static func parse_secureData(_ reader: BufferReader) -> SecureData? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Buffer? - _2 = parseBytes(reader) - var _3: Buffer? - _3 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.SecureData.secureData(data: _1!, dataHash: _2!, secret: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum SecureFile: TypeConstructorDescription { - case secureFile(id: Int64, accessHash: Int64, size: Int64, dcId: Int32, date: Int32, fileHash: Buffer, secret: Buffer) - case secureFileEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .secureFile(let id, let accessHash, let size, let dcId, let date, let fileHash, let secret): - if boxed { - buffer.appendInt32(2097791614) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt64(size, buffer: buffer, boxed: false) - serializeInt32(dcId, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeBytes(fileHash, buffer: buffer, boxed: false) - serializeBytes(secret, buffer: buffer, boxed: false) - break - case .secureFileEmpty: - if boxed { - buffer.appendInt32(1679398724) - } - - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .secureFile(let id, let accessHash, let size, let dcId, let date, let fileHash, let secret): - return ("secureFile", [("id", id as Any), ("accessHash", accessHash as Any), ("size", size as Any), ("dcId", dcId as Any), ("date", date as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) - case .secureFileEmpty: - return ("secureFileEmpty", []) - } - } - - public static func parse_secureFile(_ reader: BufferReader) -> SecureFile? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int64? - _3 = reader.readInt64() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - var _6: Buffer? - _6 = parseBytes(reader) - var _7: Buffer? - _7 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.SecureFile.secureFile(id: _1!, accessHash: _2!, size: _3!, dcId: _4!, date: _5!, fileHash: _6!, secret: _7!) - } - else { - return nil - } - } - public static func parse_secureFileEmpty(_ reader: BufferReader) -> SecureFile? { - return Api.SecureFile.secureFileEmpty - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index 3d6fc545846..a7048666112 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -1,3 +1,399 @@ +public extension Api { + enum SavedContact: TypeConstructorDescription { + case savedPhoneContact(phone: String, firstName: String, lastName: String, date: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedPhoneContact(let phone, let firstName, let lastName, let date): + if boxed { + buffer.appendInt32(289586518) + } + serializeString(phone, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedPhoneContact(let phone, let firstName, let lastName, let date): + return ("savedPhoneContact", [("phone", phone as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("date", date as Any)]) + } + } + + public static func parse_savedPhoneContact(_ reader: BufferReader) -> SavedContact? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.SavedContact.savedPhoneContact(phone: _1!, firstName: _2!, lastName: _3!, date: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum SavedDialog: TypeConstructorDescription { + case savedDialog(flags: Int32, peer: Api.Peer, topMessage: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedDialog(let flags, let peer, let topMessage): + if boxed { + buffer.appendInt32(-1115174036) + } + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(topMessage, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedDialog(let flags, let peer, let topMessage): + return ("savedDialog", [("flags", flags as Any), ("peer", peer as Any), ("topMessage", topMessage as Any)]) + } + } + + public static func parse_savedDialog(_ reader: BufferReader) -> SavedDialog? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.SavedDialog.savedDialog(flags: _1!, peer: _2!, topMessage: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum SavedReactionTag: TypeConstructorDescription { + case savedReactionTag(flags: Int32, reaction: Api.Reaction, title: String?, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .savedReactionTag(let flags, let reaction, let title, let count): + if boxed { + buffer.appendInt32(-881854424) + } + serializeInt32(flags, buffer: buffer, boxed: false) + reaction.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .savedReactionTag(let flags, let reaction, let title, let count): + return ("savedReactionTag", [("flags", flags as Any), ("reaction", reaction as Any), ("title", title as Any), ("count", count as Any)]) + } + } + + public static func parse_savedReactionTag(_ reader: BufferReader) -> SavedReactionTag? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Reaction? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Reaction + } + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.SavedReactionTag.savedReactionTag(flags: _1!, reaction: _2!, title: _3, count: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum SearchResultsCalendarPeriod: TypeConstructorDescription { + case searchResultsCalendarPeriod(date: Int32, minMsgId: Int32, maxMsgId: Int32, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .searchResultsCalendarPeriod(let date, let minMsgId, let maxMsgId, let count): + if boxed { + buffer.appendInt32(-911191137) + } + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(minMsgId, buffer: buffer, boxed: false) + serializeInt32(maxMsgId, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .searchResultsCalendarPeriod(let date, let minMsgId, let maxMsgId, let count): + return ("searchResultsCalendarPeriod", [("date", date as Any), ("minMsgId", minMsgId as Any), ("maxMsgId", maxMsgId as Any), ("count", count as Any)]) + } + } + + public static func parse_searchResultsCalendarPeriod(_ reader: BufferReader) -> SearchResultsCalendarPeriod? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.SearchResultsCalendarPeriod.searchResultsCalendarPeriod(date: _1!, minMsgId: _2!, maxMsgId: _3!, count: _4!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum SearchResultsPosition: TypeConstructorDescription { + case searchResultPosition(msgId: Int32, date: Int32, offset: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .searchResultPosition(let msgId, let date, let offset): + if boxed { + buffer.appendInt32(2137295719) + } + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(offset, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .searchResultPosition(let msgId, let date, let offset): + return ("searchResultPosition", [("msgId", msgId as Any), ("date", date as Any), ("offset", offset as Any)]) + } + } + + public static func parse_searchResultPosition(_ reader: BufferReader) -> SearchResultsPosition? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.SearchResultsPosition.searchResultPosition(msgId: _1!, date: _2!, offset: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum SecureCredentialsEncrypted: TypeConstructorDescription { + case secureCredentialsEncrypted(data: Buffer, hash: Buffer, secret: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .secureCredentialsEncrypted(let data, let hash, let secret): + if boxed { + buffer.appendInt32(871426631) + } + serializeBytes(data, buffer: buffer, boxed: false) + serializeBytes(hash, buffer: buffer, boxed: false) + serializeBytes(secret, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .secureCredentialsEncrypted(let data, let hash, let secret): + return ("secureCredentialsEncrypted", [("data", data as Any), ("hash", hash as Any), ("secret", secret as Any)]) + } + } + + public static func parse_secureCredentialsEncrypted(_ reader: BufferReader) -> SecureCredentialsEncrypted? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Buffer? + _3 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.SecureCredentialsEncrypted.secureCredentialsEncrypted(data: _1!, hash: _2!, secret: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum SecureData: TypeConstructorDescription { + case secureData(data: Buffer, dataHash: Buffer, secret: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .secureData(let data, let dataHash, let secret): + if boxed { + buffer.appendInt32(-1964327229) + } + serializeBytes(data, buffer: buffer, boxed: false) + serializeBytes(dataHash, buffer: buffer, boxed: false) + serializeBytes(secret, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .secureData(let data, let dataHash, let secret): + return ("secureData", [("data", data as Any), ("dataHash", dataHash as Any), ("secret", secret as Any)]) + } + } + + public static func parse_secureData(_ reader: BufferReader) -> SecureData? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Buffer? + _3 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.SecureData.secureData(data: _1!, dataHash: _2!, secret: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum SecureFile: TypeConstructorDescription { + case secureFile(id: Int64, accessHash: Int64, size: Int64, dcId: Int32, date: Int32, fileHash: Buffer, secret: Buffer) + case secureFileEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .secureFile(let id, let accessHash, let size, let dcId, let date, let fileHash, let secret): + if boxed { + buffer.appendInt32(2097791614) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt64(size, buffer: buffer, boxed: false) + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeBytes(fileHash, buffer: buffer, boxed: false) + serializeBytes(secret, buffer: buffer, boxed: false) + break + case .secureFileEmpty: + if boxed { + buffer.appendInt32(1679398724) + } + + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .secureFile(let id, let accessHash, let size, let dcId, let date, let fileHash, let secret): + return ("secureFile", [("id", id as Any), ("accessHash", accessHash as Any), ("size", size as Any), ("dcId", dcId as Any), ("date", date as Any), ("fileHash", fileHash as Any), ("secret", secret as Any)]) + case .secureFileEmpty: + return ("secureFileEmpty", []) + } + } + + public static func parse_secureFile(_ reader: BufferReader) -> SecureFile? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.SecureFile.secureFile(id: _1!, accessHash: _2!, size: _3!, dcId: _4!, date: _5!, fileHash: _6!, secret: _7!) + } + else { + return nil + } + } + public static func parse_secureFileEmpty(_ reader: BufferReader) -> SecureFile? { + return Api.SecureFile.secureFileEmpty + } + + } +} public extension Api { enum SecurePasswordKdfAlgo: TypeConstructorDescription { case securePasswordKdfAlgoPBKDF2HMACSHA512iter100000(salt: Buffer) diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 0d646cf4e46..83c393bc912 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -441,14 +441,14 @@ public extension Api { } } public extension Api { - enum SponsoredMessage: TypeConstructorDescription { - case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?) + indirect enum SponsoredMessage: TypeConstructorDescription { + case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, media: Api.MessageMedia?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let color, let buttonText, let sponsorInfo, let additionalInfo): + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): if boxed { - buffer.appendInt32(-1108478618) + buffer.appendInt32(1301522832) } serializeInt32(flags, buffer: buffer, boxed: false) serializeBytes(randomId, buffer: buffer, boxed: false) @@ -461,6 +461,7 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 6) != 0 {photo!.serialize(buffer, true)} + if Int(flags) & Int(1 << 14) != 0 {media!.serialize(buffer, true)} if Int(flags) & Int(1 << 13) != 0 {color!.serialize(buffer, true)} serializeString(buttonText, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 7) != 0 {serializeString(sponsorInfo!, buffer: buffer, boxed: false)} @@ -471,8 +472,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let color, let buttonText, let sponsorInfo, let additionalInfo): - return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any)]) + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let media, let color, let buttonText, let sponsorInfo, let additionalInfo): + return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("media", media as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any)]) } } @@ -495,16 +496,20 @@ public extension Api { if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Photo } } - var _8: Api.PeerColor? + var _8: Api.MessageMedia? + if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.MessageMedia + } } + var _9: Api.PeerColor? if Int(_1!) & Int(1 << 13) != 0 {if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.PeerColor + _9 = Api.parse(reader, signature: signature) as? Api.PeerColor } } - var _9: String? - _9 = parseString(reader) var _10: String? - if Int(_1!) & Int(1 << 7) != 0 {_10 = parseString(reader) } + _10 = parseString(reader) var _11: String? - if Int(_1!) & Int(1 << 8) != 0 {_11 = parseString(reader) } + if Int(_1!) & Int(1 << 7) != 0 {_11 = parseString(reader) } + var _12: String? + if Int(_1!) & Int(1 << 8) != 0 {_12 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -512,12 +517,13 @@ public extension Api { let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 13) == 0) || _8 != nil - let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 7) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 8) == 0) || _11 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { - return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, color: _8, buttonText: _9!, sponsorInfo: _10, additionalInfo: _11) + let _c8 = (Int(_1!) & Int(1 << 14) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 13) == 0) || _9 != nil + let _c10 = _10 != nil + let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { + return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12) } else { return nil @@ -670,6 +676,106 @@ public extension Api { } } +public extension Api { + enum StarsSubscription: TypeConstructorDescription { + case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing, chatInviteHash: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash): + if boxed { + buffer.appendInt32(1401868056) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(id, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(untilDate, buffer: buffer, boxed: false) + pricing.serialize(buffer, true) + if Int(flags) & Int(1 << 3) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash): + return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any), ("chatInviteHash", chatInviteHash as Any)]) + } + } + + public static func parse_starsSubscription(_ reader: BufferReader) -> StarsSubscription? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.Peer? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _4: Int32? + _4 = reader.readInt32() + var _5: Api.StarsSubscriptionPricing? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing + } + var _6: String? + if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6) + } + else { + return nil + } + } + + } +} +public extension Api { + enum StarsSubscriptionPricing: TypeConstructorDescription { + case starsSubscriptionPricing(period: Int32, amount: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsSubscriptionPricing(let period, let amount): + if boxed { + buffer.appendInt32(88173912) + } + serializeInt32(period, buffer: buffer, boxed: false) + serializeInt64(amount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsSubscriptionPricing(let period, let amount): + return ("starsSubscriptionPricing", [("period", period as Any), ("amount", amount as Any)]) + } + } + + public static func parse_starsSubscriptionPricing(_ reader: BufferReader) -> StarsSubscriptionPricing? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StarsSubscriptionPricing.starsSubscriptionPricing(period: _1!, amount: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum StarsTopupOption: TypeConstructorDescription { case starsTopupOption(flags: Int32, stars: Int64, storeProduct: String?, currency: String, amount: Int64) @@ -724,13 +830,13 @@ public extension Api { } public extension Api { enum StarsTransaction: TypeConstructorDescription { - case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?) + case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia): + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod): if boxed { - buffer.appendInt32(766853519) + buffer.appendInt32(1127934763) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -749,14 +855,15 @@ public extension Api { for item in extendedMedia! { item.serialize(buffer, true) }} + if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia): - return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any)]) + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod): + return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any)]) } } @@ -793,6 +900,8 @@ public extension Api { if Int(_1!) & Int(1 << 9) != 0 {if let _ = reader.readInt32() { _13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self) } } + var _14: Int32? + if Int(_1!) & Int(1 << 12) != 0 {_14 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -806,8 +915,9 @@ public extension Api { let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { - return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13) + let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 3ec0b4d752c..4ee341396a9 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -409,6 +409,7 @@ public extension Api { case channelAdminLogEventActionToggleInvites(newValue: Api.Bool) case channelAdminLogEventActionToggleNoForwards(newValue: Api.Bool) case channelAdminLogEventActionTogglePreHistoryHidden(newValue: Api.Bool) + case channelAdminLogEventActionToggleSignatureProfiles(newValue: Api.Bool) case channelAdminLogEventActionToggleSignatures(newValue: Api.Bool) case channelAdminLogEventActionToggleSlowMode(prevValue: Int32, newValue: Int32) case channelAdminLogEventActionUpdatePinned(message: Api.Message) @@ -718,6 +719,12 @@ public extension Api { } newValue.serialize(buffer, true) break + case .channelAdminLogEventActionToggleSignatureProfiles(let newValue): + if boxed { + buffer.appendInt32(1621597305) + } + newValue.serialize(buffer, true) + break case .channelAdminLogEventActionToggleSignatures(let newValue): if boxed { buffer.appendInt32(648939889) @@ -832,6 +839,8 @@ public extension Api { return ("channelAdminLogEventActionToggleNoForwards", [("newValue", newValue as Any)]) case .channelAdminLogEventActionTogglePreHistoryHidden(let newValue): return ("channelAdminLogEventActionTogglePreHistoryHidden", [("newValue", newValue as Any)]) + case .channelAdminLogEventActionToggleSignatureProfiles(let newValue): + return ("channelAdminLogEventActionToggleSignatureProfiles", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleSignatures(let newValue): return ("channelAdminLogEventActionToggleSignatures", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleSlowMode(let prevValue, let newValue): @@ -1505,6 +1514,19 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionToggleSignatureProfiles(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.Bool? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Bool + } + let _c1 = _1 != nil + if _c1 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionToggleSignatureProfiles(newValue: _1!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionToggleSignatures(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index 508a3b6a355..e368c5f707b 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1246,21 +1246,28 @@ public extension Api.payments { } public extension Api.payments { enum StarsStatus: TypeConstructorDescription { - case starsStatus(flags: Int32, balance: Int64, history: [Api.StarsTransaction], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) + case starsStatus(flags: Int32, balance: Int64, subscriptions: [Api.StarsSubscription]?, subscriptionsNextOffset: String?, subscriptionsMissingBalance: Int64?, history: [Api.StarsTransaction]?, nextOffset: String?, chats: [Api.Chat], users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsStatus(let flags, let balance, let history, let nextOffset, let chats, let users): + case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let subscriptionsMissingBalance, let history, let nextOffset, let chats, let users): if boxed { - buffer.appendInt32(-1930105248) + buffer.appendInt32(-1141231252) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(balance, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(history.count)) - for item in history { + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(subscriptions!.count)) + for item in subscriptions! { item.serialize(buffer, true) - } + }} + if Int(flags) & Int(1 << 2) != 0 {serializeString(subscriptionsNextOffset!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeInt64(subscriptionsMissingBalance!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 3) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(history!.count)) + for item in history! { + item.serialize(buffer, true) + }} if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} buffer.appendInt32(481674261) buffer.appendInt32(Int32(chats.count)) @@ -1278,8 +1285,8 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsStatus(let flags, let balance, let history, let nextOffset, let chats, let users): - return ("starsStatus", [("flags", flags as Any), ("balance", balance as Any), ("history", history as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) + case .starsStatus(let flags, let balance, let subscriptions, let subscriptionsNextOffset, let subscriptionsMissingBalance, let history, let nextOffset, let chats, let users): + return ("starsStatus", [("flags", flags as Any), ("balance", balance as Any), ("subscriptions", subscriptions as Any), ("subscriptionsNextOffset", subscriptionsNextOffset as Any), ("subscriptionsMissingBalance", subscriptionsMissingBalance as Any), ("history", history as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)]) } } @@ -1288,28 +1295,39 @@ public extension Api.payments { _1 = reader.readInt32() var _2: Int64? _2 = reader.readInt64() - var _3: [Api.StarsTransaction]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) - } + var _3: [Api.StarsSubscription]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsSubscription.self) + } } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 {_4 = parseString(reader) } - var _5: [Api.Chat]? + if Int(_1!) & Int(1 << 2) != 0 {_4 = parseString(reader) } + var _5: Int64? + if Int(_1!) & Int(1 << 4) != 0 {_5 = reader.readInt64() } + var _6: [Api.StarsTransaction]? + if Int(_1!) & Int(1 << 3) != 0 {if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) + } } + var _7: String? + if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) } + var _8: [Api.Chat]? if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } - var _6: [Api.User]? + var _9: [Api.User]? if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.payments.StarsStatus.starsStatus(flags: _1!, balance: _2!, history: _3!, nextOffset: _4, chats: _5!, users: _6!) + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.payments.StarsStatus.starsStatus(flags: _1!, balance: _2!, subscriptions: _3, subscriptionsNextOffset: _4, subscriptionsMissingBalance: _5, history: _6, nextOffset: _7, chats: _8!, users: _9!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 6c886c37356..dc8ee6eebb0 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -3556,12 +3556,12 @@ public extension Api.functions.channels { } } public extension Api.functions.channels { - static func toggleSignatures(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func toggleSignatures(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(527021574) + buffer.appendInt32(1099781276) + serializeInt32(flags, buffer: buffer, boxed: false) channel.serialize(buffer, true) - enabled.serialize(buffer, true) - return (FunctionDescription(name: "channels.toggleSignatures", parameters: [("channel", String(describing: channel)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "channels.toggleSignatures", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -5439,15 +5439,16 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func exportChatInvite(flags: Int32, peer: Api.InputPeer, expireDate: Int32?, usageLimit: Int32?, title: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func exportChatInvite(flags: Int32, peer: Api.InputPeer, expireDate: Int32?, usageLimit: Int32?, title: String?, subscriptionPricing: Api.StarsSubscriptionPricing?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1607670315) + buffer.appendInt32(-1537876336) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(expireDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(usageLimit!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "messages.exportChatInvite", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("expireDate", String(describing: expireDate)), ("usageLimit", String(describing: usageLimit)), ("title", String(describing: title))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ExportedChatInvite? in + if Int(flags) & Int(1 << 5) != 0 {subscriptionPricing!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.exportChatInvite", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("expireDate", String(describing: expireDate)), ("usageLimit", String(describing: usageLimit)), ("title", String(describing: title)), ("subscriptionPricing", String(describing: subscriptionPricing))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.ExportedChatInvite? in let reader = BufferReader(buffer) var result: Api.ExportedChatInvite? if let signature = reader.readInt32() { @@ -7933,6 +7934,25 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func sendPaidReaction(flags: Int32, peer: Api.InputPeer, msgId: Int32, count: Int32, randomId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(633929278) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.sendPaidReaction", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("count", String(describing: count)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.messages { static func sendQuickReplyMessages(peer: Api.InputPeer, shortcutId: Int32, id: [Int32], randomId: [Int64]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8132,14 +8152,15 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func setChatAvailableReactions(flags: Int32, peer: Api.InputPeer, availableReactions: Api.ChatReactions, reactionsLimit: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func setChatAvailableReactions(flags: Int32, peer: Api.InputPeer, availableReactions: Api.ChatReactions, reactionsLimit: Int32?, paidEnabled: Api.Bool?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1511328724) + buffer.appendInt32(-2041895551) serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) availableReactions.serialize(buffer, true) if Int(flags) & Int(1 << 0) != 0 {serializeInt32(reactionsLimit!, buffer: buffer, boxed: false)} - return (FunctionDescription(name: "messages.setChatAvailableReactions", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("availableReactions", String(describing: availableReactions)), ("reactionsLimit", String(describing: reactionsLimit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + if Int(flags) & Int(1 << 1) != 0 {paidEnabled!.serialize(buffer, true)} + return (FunctionDescription(name: "messages.setChatAvailableReactions", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("availableReactions", String(describing: availableReactions)), ("reactionsLimit", String(describing: reactionsLimit)), ("paidEnabled", String(describing: paidEnabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -8424,6 +8445,23 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func togglePaidReactionPrivacy(peer: Api.InputPeer, msgId: Int32, `private`: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-2070228073) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + `private`.serialize(buffer, true) + return (FunctionDescription(name: "messages.togglePaidReactionPrivacy", parameters: [("peer", String(describing: peer)), ("msgId", String(describing: msgId)), ("`private`", String(describing: `private`))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func togglePeerTranslations(flags: Int32, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8735,6 +8773,24 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func changeStarsSubscription(flags: Int32, peer: Api.InputPeer, subscriptionId: String, canceled: Api.Bool?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-948500360) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeString(subscriptionId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {canceled!.serialize(buffer, true)} + return (FunctionDescription(name: "payments.changeStarsSubscription", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("subscriptionId", String(describing: subscriptionId)), ("canceled", String(describing: canceled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func checkGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8780,6 +8836,22 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func fulfillStarsSubscription(peer: Api.InputPeer, subscriptionId: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-866391117) + peer.serialize(buffer, true) + serializeString(subscriptionId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.fulfillStarsSubscription", parameters: [("peer", String(describing: peer)), ("subscriptionId", String(describing: subscriptionId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func getBankCardData(number: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8954,6 +9026,23 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getStarsSubscriptions(flags: Int32, peer: Api.InputPeer, offset: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(52761285) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeString(offset, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getStarsSubscriptions", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("offset", String(describing: offset))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsStatus? in + let reader = BufferReader(buffer) + var result: Api.payments.StarsStatus? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarsStatus + } + return result + }) + } +} public extension Api.functions.payments { static func getStarsTopupOptions() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.StarsTopupOption]>) { let buffer = Buffer() @@ -8970,14 +9059,15 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func getStarsTransactions(flags: Int32, peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getStarsTransactions(flags: Int32, subscriptionId: String?, peer: Api.InputPeer, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1751937702) + buffer.appendInt32(1775912279) serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 3) != 0 {serializeString(subscriptionId!, buffer: buffer, boxed: false)} peer.serialize(buffer, true) serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "payments.getStarsTransactions", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsStatus? in + return (FunctionDescription(name: "payments.getStarsTransactions", parameters: [("flags", String(describing: flags)), ("subscriptionId", String(describing: subscriptionId)), ("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarsStatus? in let reader = BufferReader(buffer) var result: Api.payments.StarsStatus? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 09f23bb1783..adaf067144c 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -148,21 +148,23 @@ public extension Api { } public extension Api { enum ChannelParticipant: TypeConstructorDescription { - case channelParticipant(userId: Int64, date: Int32) + case channelParticipant(flags: Int32, userId: Int64, date: Int32, subscriptionUntilDate: Int32?) case channelParticipantAdmin(flags: Int32, userId: Int64, inviterId: Int64?, promotedBy: Int64, date: Int32, adminRights: Api.ChatAdminRights, rank: String?) case channelParticipantBanned(flags: Int32, peer: Api.Peer, kickedBy: Int64, date: Int32, bannedRights: Api.ChatBannedRights) case channelParticipantCreator(flags: Int32, userId: Int64, adminRights: Api.ChatAdminRights, rank: String?) case channelParticipantLeft(peer: Api.Peer) - case channelParticipantSelf(flags: Int32, userId: Int64, inviterId: Int64, date: Int32) + case channelParticipantSelf(flags: Int32, userId: Int64, inviterId: Int64, date: Int32, subscriptionUntilDate: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channelParticipant(let userId, let date): + case .channelParticipant(let flags, let userId, let date, let subscriptionUntilDate): if boxed { - buffer.appendInt32(-1072953408) + buffer.appendInt32(-885426663) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt32(subscriptionUntilDate!, buffer: buffer, boxed: false)} break case .channelParticipantAdmin(let flags, let userId, let inviterId, let promotedBy, let date, let adminRights, let rank): if boxed { @@ -201,22 +203,23 @@ public extension Api { } peer.serialize(buffer, true) break - case .channelParticipantSelf(let flags, let userId, let inviterId, let date): + case .channelParticipantSelf(let flags, let userId, let inviterId, let date, let subscriptionUntilDate): if boxed { - buffer.appendInt32(900251559) + buffer.appendInt32(1331723247) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(userId, buffer: buffer, boxed: false) serializeInt64(inviterId, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(subscriptionUntilDate!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channelParticipant(let userId, let date): - return ("channelParticipant", [("userId", userId as Any), ("date", date as Any)]) + case .channelParticipant(let flags, let userId, let date, let subscriptionUntilDate): + return ("channelParticipant", [("flags", flags as Any), ("userId", userId as Any), ("date", date as Any), ("subscriptionUntilDate", subscriptionUntilDate as Any)]) case .channelParticipantAdmin(let flags, let userId, let inviterId, let promotedBy, let date, let adminRights, let rank): return ("channelParticipantAdmin", [("flags", flags as Any), ("userId", userId as Any), ("inviterId", inviterId as Any), ("promotedBy", promotedBy as Any), ("date", date as Any), ("adminRights", adminRights as Any), ("rank", rank as Any)]) case .channelParticipantBanned(let flags, let peer, let kickedBy, let date, let bannedRights): @@ -225,20 +228,26 @@ public extension Api { return ("channelParticipantCreator", [("flags", flags as Any), ("userId", userId as Any), ("adminRights", adminRights as Any), ("rank", rank as Any)]) case .channelParticipantLeft(let peer): return ("channelParticipantLeft", [("peer", peer as Any)]) - case .channelParticipantSelf(let flags, let userId, let inviterId, let date): - return ("channelParticipantSelf", [("flags", flags as Any), ("userId", userId as Any), ("inviterId", inviterId as Any), ("date", date as Any)]) + case .channelParticipantSelf(let flags, let userId, let inviterId, let date, let subscriptionUntilDate): + return ("channelParticipantSelf", [("flags", flags as Any), ("userId", userId as Any), ("inviterId", inviterId as Any), ("date", date as Any), ("subscriptionUntilDate", subscriptionUntilDate as Any)]) } } public static func parse_channelParticipant(_ reader: BufferReader) -> ChannelParticipant? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.ChannelParticipant.channelParticipant(userId: _1!, date: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.ChannelParticipant.channelParticipant(flags: _1!, userId: _2!, date: _3!, subscriptionUntilDate: _4) } else { return nil @@ -346,12 +355,15 @@ public extension Api { _3 = reader.readInt64() var _4: Int32? _4 = reader.readInt32() + var _5: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.ChannelParticipant.channelParticipantSelf(flags: _1!, userId: _2!, inviterId: _3!, date: _4!) + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.ChannelParticipant.channelParticipantSelf(flags: _1!, userId: _2!, inviterId: _3!, date: _4!, subscriptionUntilDate: _5) } else { return nil @@ -522,7 +534,7 @@ public extension Api { } public extension Api { indirect enum Chat: TypeConstructorDescription { - case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, emojiStatus: Api.EmojiStatus?, level: Int32?) + case channel(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, title: String, username: String?, photo: Api.ChatPhoto, date: Int32, restrictionReason: [Api.RestrictionReason]?, adminRights: Api.ChatAdminRights?, bannedRights: Api.ChatBannedRights?, defaultBannedRights: Api.ChatBannedRights?, participantsCount: Int32?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, emojiStatus: Api.EmojiStatus?, level: Int32?, subscriptionUntilDate: Int32?) case channelForbidden(flags: Int32, id: Int64, accessHash: Int64, title: String, untilDate: Int32?) case chat(flags: Int32, id: Int64, title: String, photo: Api.ChatPhoto, participantsCount: Int32, date: Int32, version: Int32, migratedTo: Api.InputChannel?, adminRights: Api.ChatAdminRights?, defaultBannedRights: Api.ChatBannedRights?) case chatEmpty(id: Int64) @@ -530,9 +542,9 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color, let profileColor, let emojiStatus, let level): + case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color, let profileColor, let emojiStatus, let level, let subscriptionUntilDate): if boxed { - buffer.appendInt32(179174543) + buffer.appendInt32(-29067075) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false) @@ -561,6 +573,7 @@ public extension Api { if Int(flags2) & Int(1 << 8) != 0 {profileColor!.serialize(buffer, true)} if Int(flags2) & Int(1 << 9) != 0 {emojiStatus!.serialize(buffer, true)} if Int(flags2) & Int(1 << 10) != 0 {serializeInt32(level!, buffer: buffer, boxed: false)} + if Int(flags2) & Int(1 << 11) != 0 {serializeInt32(subscriptionUntilDate!, buffer: buffer, boxed: false)} break case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): if boxed { @@ -605,8 +618,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color, let profileColor, let emojiStatus, let level): - return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("emojiStatus", emojiStatus as Any), ("level", level as Any)]) + case .channel(let flags, let flags2, let id, let accessHash, let title, let username, let photo, let date, let restrictionReason, let adminRights, let bannedRights, let defaultBannedRights, let participantsCount, let usernames, let storiesMaxId, let color, let profileColor, let emojiStatus, let level, let subscriptionUntilDate): + return ("channel", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("username", username as Any), ("photo", photo as Any), ("date", date as Any), ("restrictionReason", restrictionReason as Any), ("adminRights", adminRights as Any), ("bannedRights", bannedRights as Any), ("defaultBannedRights", defaultBannedRights as Any), ("participantsCount", participantsCount as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("emojiStatus", emojiStatus as Any), ("level", level as Any), ("subscriptionUntilDate", subscriptionUntilDate as Any)]) case .channelForbidden(let flags, let id, let accessHash, let title, let untilDate): return ("channelForbidden", [("flags", flags as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("title", title as Any), ("untilDate", untilDate as Any)]) case .chat(let flags, let id, let title, let photo, let participantsCount, let date, let version, let migratedTo, let adminRights, let defaultBannedRights): @@ -675,6 +688,8 @@ public extension Api { } } var _19: Int32? if Int(_2!) & Int(1 << 10) != 0 {_19 = reader.readInt32() } + var _20: Int32? + if Int(_2!) & Int(1 << 11) != 0 {_20 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -694,8 +709,9 @@ public extension Api { let _c17 = (Int(_2!) & Int(1 << 8) == 0) || _17 != nil let _c18 = (Int(_2!) & Int(1 << 9) == 0) || _18 != nil let _c19 = (Int(_2!) & Int(1 << 10) == 0) || _19 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 { - return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14, storiesMaxId: _15, color: _16, profileColor: _17, emojiStatus: _18, level: _19) + let _c20 = (Int(_2!) & Int(1 << 11) == 0) || _20 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 { + return Api.Chat.channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14, storiesMaxId: _15, color: _16, profileColor: _17, emojiStatus: _18, level: _19, subscriptionUntilDate: _20) } else { return nil @@ -1280,15 +1296,15 @@ public extension Api { } public extension Api { indirect enum ChatInvite: TypeConstructorDescription { - case chatInvite(flags: Int32, title: String, about: String?, photo: Api.Photo, participantsCount: Int32, participants: [Api.User]?, color: Int32) + case chatInvite(flags: Int32, title: String, about: String?, photo: Api.Photo, participantsCount: Int32, participants: [Api.User]?, color: Int32, subscriptionPricing: Api.StarsSubscriptionPricing?, subscriptionFormId: Int64?) case chatInviteAlready(chat: Api.Chat) case chatInvitePeek(chat: Api.Chat, expires: Int32) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color): + case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color, let subscriptionPricing, let subscriptionFormId): if boxed { - buffer.appendInt32(-840897472) + buffer.appendInt32(-26920803) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false) @@ -1301,6 +1317,8 @@ public extension Api { item.serialize(buffer, true) }} serializeInt32(color, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 10) != 0 {subscriptionPricing!.serialize(buffer, true)} + if Int(flags) & Int(1 << 12) != 0 {serializeInt64(subscriptionFormId!, buffer: buffer, boxed: false)} break case .chatInviteAlready(let chat): if boxed { @@ -1320,8 +1338,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color): - return ("chatInvite", [("flags", flags as Any), ("title", title as Any), ("about", about as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("participants", participants as Any), ("color", color as Any)]) + case .chatInvite(let flags, let title, let about, let photo, let participantsCount, let participants, let color, let subscriptionPricing, let subscriptionFormId): + return ("chatInvite", [("flags", flags as Any), ("title", title as Any), ("about", about as Any), ("photo", photo as Any), ("participantsCount", participantsCount as Any), ("participants", participants as Any), ("color", color as Any), ("subscriptionPricing", subscriptionPricing as Any), ("subscriptionFormId", subscriptionFormId as Any)]) case .chatInviteAlready(let chat): return ("chatInviteAlready", [("chat", chat as Any)]) case .chatInvitePeek(let chat, let expires): @@ -1348,6 +1366,12 @@ public extension Api { } } var _7: Int32? _7 = reader.readInt32() + var _8: Api.StarsSubscriptionPricing? + if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing + } } + var _9: Int64? + if Int(_1!) & Int(1 << 12) != 0 {_9 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil @@ -1355,8 +1379,10 @@ public extension Api { let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.ChatInvite.chatInvite(flags: _1!, title: _2!, about: _3, photo: _4!, participantsCount: _5!, participants: _6, color: _7!) + let _c8 = (Int(_1!) & Int(1 << 10) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 12) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.ChatInvite.chatInvite(flags: _1!, title: _2!, about: _3, photo: _4!, participantsCount: _5!, participants: _6, color: _7!, subscriptionPricing: _8, subscriptionFormId: _9) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 177e11efdea..aae395e494b 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -1012,14 +1012,14 @@ public extension Api { } public extension Api { enum ExportedChatInvite: TypeConstructorDescription { - case chatInviteExported(flags: Int32, link: String, adminId: Int64, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, usage: Int32?, requested: Int32?, title: String?) + case chatInviteExported(flags: Int32, link: String, adminId: Int64, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, usage: Int32?, requested: Int32?, subscriptionExpired: Int32?, title: String?, subscriptionPricing: Api.StarsSubscriptionPricing?) case chatInvitePublicJoinRequests public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let title): + case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let subscriptionExpired, let title, let subscriptionPricing): if boxed { - buffer.appendInt32(179611673) + buffer.appendInt32(-1574126186) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(link, buffer: buffer, boxed: false) @@ -1030,7 +1030,9 @@ public extension Api { if Int(flags) & Int(1 << 2) != 0 {serializeInt32(usageLimit!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt32(usage!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 7) != 0 {serializeInt32(requested!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 10) != 0 {serializeInt32(subscriptionExpired!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 9) != 0 {subscriptionPricing!.serialize(buffer, true)} break case .chatInvitePublicJoinRequests: if boxed { @@ -1043,8 +1045,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let title): - return ("chatInviteExported", [("flags", flags as Any), ("link", link as Any), ("adminId", adminId as Any), ("date", date as Any), ("startDate", startDate as Any), ("expireDate", expireDate as Any), ("usageLimit", usageLimit as Any), ("usage", usage as Any), ("requested", requested as Any), ("title", title as Any)]) + case .chatInviteExported(let flags, let link, let adminId, let date, let startDate, let expireDate, let usageLimit, let usage, let requested, let subscriptionExpired, let title, let subscriptionPricing): + return ("chatInviteExported", [("flags", flags as Any), ("link", link as Any), ("adminId", adminId as Any), ("date", date as Any), ("startDate", startDate as Any), ("expireDate", expireDate as Any), ("usageLimit", usageLimit as Any), ("usage", usage as Any), ("requested", requested as Any), ("subscriptionExpired", subscriptionExpired as Any), ("title", title as Any), ("subscriptionPricing", subscriptionPricing as Any)]) case .chatInvitePublicJoinRequests: return ("chatInvitePublicJoinRequests", []) } @@ -1069,8 +1071,14 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt32() } var _9: Int32? if Int(_1!) & Int(1 << 7) != 0 {_9 = reader.readInt32() } - var _10: String? - if Int(_1!) & Int(1 << 8) != 0 {_10 = parseString(reader) } + var _10: Int32? + if Int(_1!) & Int(1 << 10) != 0 {_10 = reader.readInt32() } + var _11: String? + if Int(_1!) & Int(1 << 8) != 0 {_11 = parseString(reader) } + var _12: Api.StarsSubscriptionPricing? + if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { + _12 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1080,9 +1088,11 @@ public extension Api { let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil let _c9 = (Int(_1!) & Int(1 << 7) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 8) == 0) || _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.ExportedChatInvite.chatInviteExported(flags: _1!, link: _2!, adminId: _3!, date: _4!, startDate: _5, expireDate: _6, usageLimit: _7, usage: _8, requested: _9, title: _10) + let _c10 = (Int(_1!) & Int(1 << 10) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 8) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 9) == 0) || _12 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { + return Api.ExportedChatInvite.chatInviteExported(flags: _1!, link: _2!, adminId: _3!, date: _4!, startDate: _5, expireDate: _6, usageLimit: _7, usage: _8, requested: _9, subscriptionExpired: _10, title: _11, subscriptionPricing: _12) } else { return nil diff --git a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 43e3c87f3ab..80ae771ecf4 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -31,7 +31,7 @@ private func presentLiveLocationController(context: AccountContext, peerId: Peer if let message = message, let strongController = controller { let _ = context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message._asMessage(), standalone: false, reverseMessageGalleryOrder: false, navigationController: strongController.navigationController as? NavigationController, modal: true, dismissInput: { controller?.view.endEditing(true) - }, present: { c, a in + }, present: { c, a, _ in controller?.present(c, in: .window(.root), with: a, blockInteraction: true) }, transitionNode: { _, _, _ in return nil diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index b4a2ac72395..3f8d31e7b10 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -1254,7 +1254,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } } else { if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { @@ -1362,7 +1362,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } })) } else if case let .legacyGroup(groupPeer) = groupPeer { @@ -1430,7 +1430,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } })) } @@ -2262,7 +2262,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController return } let text = strongSelf.presentationData.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) } })) @@ -2277,7 +2277,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController } else { text = strongSelf.presentationData.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string } - strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: text, action: nil, duration: 3), action: { _ in return false }) + strongSelf.presentUndoOverlay(content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false }) })) self.stateVersionDisposable.set((self.call.stateVersion diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index ce0867744c2..d79353be1e5 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -478,7 +478,7 @@ struct AccountMutableState { for chat in chats { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _): if let participantsCount = participantsCount { self.addOperation(.UpdateCachedPeerData(chat.peerId, { current in var previous: CachedChannelData diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 64b6d8a5e3d..10ae195bcbd 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -195,6 +195,7 @@ private var declaredEncodables: Void = { declareEncodable(ReplyThreadMessageAttribute.self, f: { ReplyThreadMessageAttribute(decoder: $0) }) declareEncodable(ReactionsMessageAttribute.self, f: { ReactionsMessageAttribute(decoder: $0) }) declareEncodable(PendingReactionsMessageAttribute.self, f: { PendingReactionsMessageAttribute(decoder: $0) }) + declareEncodable(PendingStarsReactionsMessageAttribute.self, f: { PendingStarsReactionsMessageAttribute(decoder: $0) }) declareEncodable(CloudDocumentMediaResource.self, f: { CloudDocumentMediaResource(decoder: $0) }) declareEncodable(TelegramMediaWebpage.self, f: { TelegramMediaWebpage(decoder: $0) }) declareEncodable(ViewCountMessageAttribute.self, f: { ViewCountMessageAttribute(decoder: $0) }) @@ -274,6 +275,7 @@ private var declaredEncodables: Void = { declareEncodable(WasScheduledMessageAttribute.self, f: { WasScheduledMessageAttribute(decoder: $0) }) declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) }) declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) }) + declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) }) declareEncodable(RestrictedContentMessageAttribute.self, f: { RestrictedContentMessageAttribute(decoder: $0) }) declareEncodable(SendScheduledMessageImmediatelyAction.self, f: { SendScheduledMessageImmediatelyAction(decoder: $0) }) declareEncodable(EmbeddedMediaStickersMessageAttribute.self, f: { EmbeddedMediaStickersMessageAttribute(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift index 3f57b53351e..ad5dae0a832 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/AdMessageAttribute.swift @@ -14,8 +14,9 @@ public final class AdMessageAttribute: MessageAttribute { public let sponsorInfo: String? public let additionalInfo: String? public let canReport: Bool + public let hasContentMedia: Bool - public init(opaqueId: Data, messageType: MessageType, url: String, buttonText: String, sponsorInfo: String?, additionalInfo: String?, canReport: Bool) { + public init(opaqueId: Data, messageType: MessageType, url: String, buttonText: String, sponsorInfo: String?, additionalInfo: String?, canReport: Bool, hasContentMedia: Bool) { self.opaqueId = opaqueId self.messageType = messageType self.url = url @@ -23,6 +24,7 @@ public final class AdMessageAttribute: MessageAttribute { self.sponsorInfo = sponsorInfo self.additionalInfo = additionalInfo self.canReport = canReport + self.hasContentMedia = hasContentMedia } public init(decoder: PostboxDecoder) { diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index a288585d959..396a271ecc1 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -61,7 +61,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) case let .chatForbidden(id, title): return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) - case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel): + case let .channel(flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel, subscriptionUntilDate): let isMin = (flags & (1 << 12)) != 0 let participationStatus: TelegramChannelParticipationStatus @@ -85,6 +85,9 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { if (flags & Int32(1 << 11)) != 0 { infoFlags.insert(.messagesShouldHaveSignatures) } + if (flags2 & Int32(1 << 12)) != 0 { + infoFlags.insert(.messagesShouldHaveProfiles) + } if (flags & Int32(1 << 20)) != 0 { infoFlags.insert(.hasDiscussionGroup) } @@ -173,7 +176,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { } } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), approximateBoostLevel: boostLevel) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), approximateBoostLevel: boostLevel, subscriptionUntilDate: subscriptionUntilDate) case let .channelForbidden(flags, id, accessHash, title, untilDate): let info: TelegramChannelInfo if (flags & Int32(1 << 8)) != 0 { @@ -182,7 +185,7 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { info = .broadcast(TelegramChannelBroadcastInfo(flags: [])) } - return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil) + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: TelegramChannelFlags(), restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil) } } @@ -190,7 +193,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { switch rhs { case .chat, .chatEmpty, .chatForbidden, .channelForbidden: return parseTelegramGroupOrChannel(chat: rhs) - case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel): + case let .channel(flags, flags2, _, accessHash, title, username, photo, _, _, _, _, defaultBannedRights, _, usernames, _, color, profileColor, emojiStatus, boostLevel, subscriptionUntilDate): let isMin = (flags & (1 << 12)) != 0 if accessHash != nil && !isMin { return parseTelegramGroupOrChannel(chat: rhs) @@ -254,7 +257,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? { let parsedEmojiStatus = emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)) - return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: parsedEmojiStatus, approximateBoostLevel: boostLevel) + return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: parsedEmojiStatus, approximateBoostLevel: boostLevel, subscriptionUntilDate: subscriptionUntilDate) } else { return parseTelegramGroupOrChannel(chat: rhs) } @@ -308,6 +311,6 @@ func mergeChannel(lhs: TelegramChannel?, rhs: TelegramChannel) -> TelegramChanne let storiesHidden: Bool? = rhs.storiesHidden ?? lhs.storiesHidden - return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, emojiStatus: rhs.emojiStatus, approximateBoostLevel: rhs.approximateBoostLevel) + return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, emojiStatus: rhs.emojiStatus, approximateBoostLevel: rhs.approximateBoostLevel, subscriptionUntilDate: rhs.subscriptionUntilDate) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift index 7ee722d35de..440f522cfed 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/BotInfo.swift @@ -16,7 +16,7 @@ extension BotMenuButton { extension BotInfo { convenience init(apiBotInfo: Api.BotInfo) { switch apiBotInfo { - case let .botInfo(_, _, description, descriptionPhoto, descriptionDocument, apiCommands, apiMenuButton): + case let .botInfo(_, _, description, descriptionPhoto, descriptionDocument, apiCommands, apiMenuButton, privacyPolicyUrl): let photo: TelegramMediaImage? = descriptionPhoto.flatMap(telegramMediaImageFromApiPhoto) let video: TelegramMediaFile? = descriptionDocument.flatMap(telegramMediaFileFromApiDocument) var commands: [BotCommand] = [] @@ -32,7 +32,7 @@ extension BotInfo { if let apiMenuButton = apiMenuButton { menuButton = BotMenuButton(apiBotMenuButton: apiMenuButton) } - self.init(description: description ?? "", photo: photo, video: video, commands: commands, menuButton: menuButton) + self.init(description: description ?? "", photo: photo, video: video, commands: commands, menuButton: menuButton, privacyPolicyUrl: privacyPolicyUrl) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift index 64f93c7ed69..2adf13b3346 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/CachedChannelParticipants.swift @@ -72,13 +72,13 @@ public struct ChannelParticipantBannedInfo: PostboxCoding, Equatable { public enum ChannelParticipant: PostboxCoding, Equatable { case creator(id: PeerId, adminInfo: ChannelParticipantAdminInfo?, rank: String?) - case member(id: PeerId, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?) + case member(id: PeerId, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?, subscriptionUntilDate: Int32?) public var peerId: PeerId { switch self { case let .creator(id, _, _): return id - case let .member(id, _, _, _, _): + case let .member(id, _, _, _, _, _): return id } } @@ -87,15 +87,15 @@ public enum ChannelParticipant: PostboxCoding, Equatable { switch self { case let .creator(_, _, rank): return rank - case let .member(_, _, _, _, rank): + case let .member(_, _, _, _, rank, _): return rank } } public static func ==(lhs: ChannelParticipant, rhs: ChannelParticipant) -> Bool { switch lhs { - case let .member(lhsId, lhsInvitedAt, lhsAdminInfo, lhsBanInfo, lhsRank): - if case let .member(rhsId, rhsInvitedAt, rhsAdminInfo, rhsBanInfo, rhsRank) = rhs { + case let .member(lhsId, lhsInvitedAt, lhsAdminInfo, lhsBanInfo, lhsRank, lhsSubscriptionUntilDate): + if case let .member(rhsId, rhsInvitedAt, rhsAdminInfo, rhsBanInfo, rhsRank, rhsSubscriptionUntilDate) = rhs { if lhsId != rhsId { return false } @@ -111,6 +111,9 @@ public enum ChannelParticipant: PostboxCoding, Equatable { if lhsRank != rhsRank { return false } + if lhsSubscriptionUntilDate != rhsSubscriptionUntilDate { + return false + } return true } else { return false @@ -127,17 +130,17 @@ public enum ChannelParticipant: PostboxCoding, Equatable { public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { case ChannelParticipantValue.member.rawValue: - self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, banInfo: decoder.decodeObjectForKey("bi", decoder: { ChannelParticipantBannedInfo(decoder: $0) }) as? ChannelParticipantBannedInfo, rank: decoder.decodeOptionalStringForKey("rank")) + self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, banInfo: decoder.decodeObjectForKey("bi", decoder: { ChannelParticipantBannedInfo(decoder: $0) }) as? ChannelParticipantBannedInfo, rank: decoder.decodeOptionalStringForKey("rank"), subscriptionUntilDate: decoder.decodeOptionalInt32ForKey("subscriptionUntilDate")) case ChannelParticipantValue.creator.rawValue: self = .creator(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, rank: decoder.decodeOptionalStringForKey("rank")) default: - self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil) } } public func encode(_ encoder: PostboxEncoder) { switch self { - case let .member(id, invitedAt, adminInfo, banInfo, rank): + case let .member(id, invitedAt, adminInfo, banInfo, rank, subscriptionUntilDate): encoder.encodeInt32(ChannelParticipantValue.member.rawValue, forKey: "r") encoder.encodeInt64(id.toInt64(), forKey: "i") encoder.encodeInt32(invitedAt, forKey: "t") @@ -156,6 +159,11 @@ public enum ChannelParticipant: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "rank") } + if let subscriptionUntilDate = subscriptionUntilDate { + encoder.encodeInt32(subscriptionUntilDate, forKey: "subscriptionUntilDate") + } else { + encoder.encodeNil(forKey: "subscriptionUntilDate") + } case let .creator(id, adminInfo, rank): encoder.encodeInt32(ChannelParticipantValue.creator.rawValue, forKey: "r") encoder.encodeInt64(id.toInt64(), forKey: "i") @@ -197,20 +205,20 @@ public final class CachedChannelParticipants: PostboxCoding, Equatable { extension ChannelParticipant { init(apiParticipant: Api.ChannelParticipant) { switch apiParticipant { - case let .channelParticipant(userId, date): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) + case let .channelParticipant(_, userId, date, subscriptionUntilDate): + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: subscriptionUntilDate) case let .channelParticipantCreator(_, userId, adminRights, rank): self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), canBeEditedByAccountPeer: true), rank: rank) case let .channelParticipantBanned(flags, userId, restrictedBy, date, bannedRights): let hasLeft = (flags & (1 << 0)) != 0 let banInfo = ChannelParticipantBannedInfo(rights: TelegramChatBannedRights(apiBannedRights: bannedRights), restrictedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(restrictedBy)), timestamp: date, isMember: !hasLeft) - self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil) + self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: nil, subscriptionUntilDate: nil) case let .channelParticipantAdmin(flags, userId, _, promotedBy, date, adminRights, rank: rank): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank) - case let .channelParticipantSelf(_, userId, _, date): - self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank, subscriptionUntilDate: nil) + case let .channelParticipantSelf(_, userId, _, date, subscriptionUntilDate): + self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: subscriptionUntilDate) case let .channelParticipantLeft(userId): - self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil) + self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift b/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift index ace0b15845b..e3584576373 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ExportedInvitation.swift @@ -6,8 +6,9 @@ import TelegramApi extension ExportedInvitation { init(apiExportedInvite: Api.ExportedChatInvite) { switch apiExportedInvite { - case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage, requested, title): - self = .link(link: link, title: title, isPermanent: (flags & (1 << 5)) != 0, requestApproval: (flags & (1 << 6)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage, requestedCount: requested) + case let .chatInviteExported(flags, link, adminId, date, startDate, expireDate, usageLimit, usage, requested, subscriptionExpired, title, pricing): + let _ = subscriptionExpired + self = .link(link: link, title: title, isPermanent: (flags & (1 << 5)) != 0, requestApproval: (flags & (1 << 6)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage, requestedCount: requested, pricing: pricing.flatMap { StarsSubscriptionPricing(apiStarsSubscriptionPricing: $0) }) case .chatInvitePublicJoinRequests: self = .publicJoinRequest } @@ -17,7 +18,7 @@ extension ExportedInvitation { public extension ExportedInvitation { var link: String? { switch self { - case let .link(link, _, _, _, _, _, _, _, _, _, _, _): + case let .link(link, _, _, _, _, _, _, _, _, _, _, _, _): return link case .publicJoinRequest: return nil @@ -26,7 +27,7 @@ public extension ExportedInvitation { var date: Int32? { switch self { - case let .link(_, _, _, _, _, _, date, _, _, _, _, _): + case let .link(_, _, _, _, _, _, date, _, _, _, _, _, _): return date case .publicJoinRequest: return nil @@ -35,7 +36,7 @@ public extension ExportedInvitation { var isPermanent: Bool { switch self { - case let .link(_, _, isPermanent, _, _, _, _, _, _, _, _, _): + case let .link(_, _, isPermanent, _, _, _, _, _, _, _, _, _, _): return isPermanent case .publicJoinRequest: return false @@ -44,10 +45,19 @@ public extension ExportedInvitation { var isRevoked: Bool { switch self { - case let .link(_, _, _, _, isRevoked, _, _, _, _, _, _, _): + case let .link(_, _, _, _, isRevoked, _, _, _, _, _, _, _, _): return isRevoked case .publicJoinRequest: return false } } + + var pricing: StarsSubscriptionPricing? { + switch self { + case let .link(_, _, _, _, _, _, _, _, _, _, _, _, pricing): + return pricing + case .publicJoinRequest: + return nil + } + } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift index a1911091afe..7734e46b46c 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ReactionsMessageAttribute.swift @@ -5,7 +5,7 @@ import TelegramApi extension ReactionsMessageAttribute { func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute { switch reactions { - case let .messageReactions(flags, results, recentReactions): + case let .messageReactions(flags, results, recentReactions, topReactors): let min = (flags & (1 << 0)) != 0 let canViewList = (flags & (1 << 2)) != 0 let isTags = (flags & (1 << 3)) != 0 @@ -54,7 +54,24 @@ extension ReactionsMessageAttribute { } } } - return ReactionsMessageAttribute(canViewList: canViewList, isTags: isTags, reactions: reactions, recentPeers: parsedRecentReactions) + + var topPeers: [ReactionsMessageAttribute.TopPeer] = [] + if let topReactors { + for item in topReactors { + switch item { + case let .messageReactor(flags, peerId, count): + topPeers.append(ReactionsMessageAttribute.TopPeer( + peerId: peerId?.peerId, + count: count, + isTop: (flags & (1 << 0)) != 0, + isMy: (flags & (1 << 1)) != 0, + isAnonymous: (flags & (1 << 2)) != 0 + )) + } + } + } + + return ReactionsMessageAttribute(canViewList: canViewList, isTags: isTags, reactions: reactions, recentPeers: parsedRecentReactions, topPeers: topPeers) } } } @@ -94,26 +111,7 @@ public func mergedMessageReactionsAndPeers(accountPeerId: EnginePeer.Id, account } } - #if DEBUG - var reactions = attribute.reactions - if "".isEmpty { - if let index = reactions.firstIndex(where: { - if case .custom(MessageReaction.starsReactionId) = $0.value { - return true - } else { - return false - } - }) { - let value = reactions[index] - reactions.remove(at: index) - reactions.insert(value, at: 0) - } else { - reactions.insert(MessageReaction(value: .custom(MessageReaction.starsReactionId), count: 1000000, chosenOrder: nil), at: 0) - } - } - #else let reactions = attribute.reactions - #endif return (reactions, recentPeers) } @@ -149,7 +147,7 @@ private func mergeReactions(reactions: [MessageReaction], recentPeers: [Reaction for i in (0 ..< result.count).reversed() { if result[i].chosenOrder != nil { - if !pending.contains(where: { $0.value == result[i].value }) { + if !pending.contains(where: { $0.value == result[i].value }), result[i].value != .stars { if let index = recentPeers.firstIndex(where: { $0.value == result[i].value && ($0.peerId == accountPeerId || $0.isMy) }) { recentPeers.remove(at: index) } @@ -181,14 +179,18 @@ public func mergedMessageReactions(attributes: [MessageAttribute], isTags: Bool) var current: ReactionsMessageAttribute? var pending: PendingReactionsMessageAttribute? + var pendingStars: PendingStarsReactionsMessageAttribute? for attribute in attributes { if let attribute = attribute as? ReactionsMessageAttribute { current = attribute } else if let attribute = attribute as? PendingReactionsMessageAttribute { pending = attribute + } else if let attribute = attribute as? PendingStarsReactionsMessageAttribute { + pendingStars = attribute } } + let result: ReactionsMessageAttribute? if let pending = pending, let accountPeerId = pending.accountPeerId { var reactions = current?.reactions ?? [] var recentPeers = current?.recentPeers ?? [] @@ -198,21 +200,44 @@ public func mergedMessageReactions(attributes: [MessageAttribute], isTags: Bool) recentPeers = updatedRecentPeers if !reactions.isEmpty { - return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: recentPeers) + result = ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: recentPeers, topPeers: current?.topPeers ?? []) } else { - return nil + result = nil } - } else if let current = current { - return current + } else if let current { + result = current } else { - return nil + result = nil + } + + if let pendingStars { + if let result { + var reactions = result.reactions + var updatedCount: Int32 = pendingStars.count + if let index = reactions.firstIndex(where: { $0.value == .stars }) { + updatedCount += reactions[index].count + reactions.remove(at: index) + } + var topPeers = result.topPeers + if let index = topPeers.firstIndex(where: { $0.isMy }) { + topPeers[index].count += pendingStars.count + } else { + topPeers.append(ReactionsMessageAttribute.TopPeer(peerId: pendingStars.accountPeerId, count: pendingStars.count, isTop: false, isMy: true, isAnonymous: pendingStars.isAnonymous)) + } + reactions.insert(MessageReaction(value: .stars, count: updatedCount, chosenOrder: -1), at: 0) + return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: result.recentPeers, topPeers: topPeers) + } else { + return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: [MessageReaction(value: .stars, count: pendingStars.count, chosenOrder: -1)], recentPeers: [], topPeers: [ReactionsMessageAttribute.TopPeer(peerId: pendingStars.accountPeerId, count: pendingStars.count, isTop: false, isMy: true, isAnonymous: pendingStars.isAnonymous)]) + } + } else { + return result } } extension ReactionsMessageAttribute { convenience init(apiReactions: Api.MessageReactions) { switch apiReactions { - case let .messageReactions(flags, results, recentReactions): + case let .messageReactions(flags, results, recentReactions, topReactors): let canViewList = (flags & (1 << 2)) != 0 let isTags = (flags & (1 << 3)) != 0 let parsedRecentReactions: [ReactionsMessageAttribute.RecentPeer] @@ -234,6 +259,22 @@ extension ReactionsMessageAttribute { parsedRecentReactions = [] } + var topPeers: [ReactionsMessageAttribute.TopPeer] = [] + if let topReactors { + for item in topReactors { + switch item { + case let .messageReactor(flags, peerId, count): + topPeers.append(ReactionsMessageAttribute.TopPeer( + peerId: peerId?.peerId, + count: count, + isTop: (flags & (1 << 0)) != 0, + isMy: (flags & (1 << 1)) != 0, + isAnonymous: (flags & (1 << 2)) != 0 + )) + } + } + } + self.init( canViewList: canViewList, isTags: isTags, @@ -247,7 +288,8 @@ extension ReactionsMessageAttribute { } } }, - recentPeers: parsedRecentReactions + recentPeers: parsedRecentReactions, + topPeers: topPeers ) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index de64ee2fc1a..abd071c9dbe 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -720,7 +720,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } } - let authorId: PeerId? + var authorId: PeerId? if let sendAsPeer = sendAsPeer { authorId = sendAsPeer.id } else if let peer = peer as? TelegramChannel { @@ -748,8 +748,16 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, if messageNamespace != Namespaces.Message.ScheduledLocal && messageNamespace != Namespaces.Message.QuickReplyLocal { attributes.append(ViewCountMessageAttribute(count: 1)) } + if info.flags.contains(.messagesShouldHaveProfiles) { + if sendAsPeer == nil { + authorId = account.peerId + } + } if info.flags.contains(.messagesShouldHaveSignatures) { - attributes.append(AuthorSignatureMessageAttribute(signature: accountPeer.debugDisplayTitle)) + if let sendAsPeer, sendAsPeer.id == peerId { + } else { + attributes.append(AuthorSignatureMessageAttribute(signature: accountPeer.debugDisplayTitle)) + } } case .group: break diff --git a/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift b/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift index 4069435c748..4d15398c114 100644 --- a/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift +++ b/submodules/TelegramCore/Sources/Settings/ReactionSettings.swift @@ -22,6 +22,8 @@ public extension ReactionSettings { } else { return ReactionSettings.default.quickReaction } + case .stars: + return self.quickReaction } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 7e67b1668b5..a248055e93b 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1699,14 +1699,14 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateCachedPeerData(peer.peerId, { current in if peer.peerId.namespace == Namespaces.Peer.CloudUser, let previous = current as? CachedUserData { if let botInfo = previous.botInfo { - return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: commands, menuButton: botInfo.menuButton)) + return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: commands, menuButton: botInfo.menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl)) } } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup, let previous = current as? CachedGroupData { if let index = previous.botInfos.firstIndex(where: { $0.peerId == botPeerId }) { var updatedBotInfos = previous.botInfos let previousBotInfo = updatedBotInfos[index] updatedBotInfos.remove(at: index) - updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton)), at: index) + updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton, privacyPolicyUrl: previousBotInfo.botInfo.privacyPolicyUrl)), at: index) return previous.withUpdatedBotInfos(updatedBotInfos) } } else if peer.peerId.namespace == Namespaces.Peer.CloudChannel, let previous = current as? CachedChannelData { @@ -1714,7 +1714,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: var updatedBotInfos = previous.botInfos let previousBotInfo = updatedBotInfos[index] updatedBotInfos.remove(at: index) - updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton)), at: index) + updatedBotInfos.insert(CachedPeerBotInfo(peerId: botPeerId, botInfo: BotInfo(description: previousBotInfo.botInfo.description, photo: previousBotInfo.botInfo.photo, video: previousBotInfo.botInfo.video, commands: commands, menuButton: previousBotInfo.botInfo.menuButton, privacyPolicyUrl: previousBotInfo.botInfo.privacyPolicyUrl)), at: index) return previous.withUpdatedBotInfos(updatedBotInfos) } } @@ -1726,7 +1726,7 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.updateCachedPeerData(botPeerId, { current in if let previous = current as? CachedUserData { if let botInfo = previous.botInfo { - return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: menuButton)) + return previous.withUpdatedBotInfo(BotInfo(description: botInfo.description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl)) } } return current diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index f2f7f5321f0..1ae6baf6ea4 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -292,6 +292,11 @@ public final class AccountStateManager { return self.botPreviewUpdatesPipe.signal() } + fileprivate let forceSendPendingStarsReactionPipe = ValuePipe() + public var forceSendPendingStarsReaction: Signal { + return self.forceSendPendingStarsReactionPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() @@ -1873,11 +1878,25 @@ public final class AccountStateManager { } } + var forceSendPendingStarsReaction: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.forceSendPendingStarsReaction.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + + func forceSendPendingStarsReaction(messageId: MessageId) { + self.impl.with { impl in + impl.forceSendPendingStarsReactionPipe.putNext(messageId) + } + } + var updateConfigRequested: (() -> Void)? var isPremiumUpdated: (() -> Void)? let messagesRemovedContext = MessagesRemovedContext() + public weak var starsContext: StarsContext? + init( accountPeerId: PeerId, accountManager: AccountManager, diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index be1cbc0efca..a0f2ee88a9d 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -87,6 +87,7 @@ final class AccountTaskManager { tasks.add(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedSynchronizeMarkAllUnseenReactionsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedApplyPendingMessageReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) + tasks.add(managedApplyPendingMessageStarsReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedApplyPendingScheduledMessagesActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedSynchronizeAvailableReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index 7476c2374f3..d0a4f149a09 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -3,6 +3,45 @@ import TelegramApi import Postbox import SwiftSignalKit +private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> TelegramMediaFile { + let baseId: Int64 = 52343278047832950 + 10 + let fileId = baseId + Int64(kind) + + var attributes: [TelegramMediaFileAttribute] = [] + attributes.append(TelegramMediaFileAttribute.FileName(fileName: isAnimatedSticker ? "sticker.tgs" : "sticker.webp")) + if !isAnimatedSticker { + attributes.append(.CustomEmoji(isPremium: false, isSingleColor: false, alt: ".", packReference: nil)) + } + + return TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: fileId), + partialReference: nil, + resource: LocalFileMediaResource(fileId: fileId), + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: isAnimatedSticker ? "application/x-tgsticker" : "image/webp", + size: nil, + attributes: attributes + ) +} + +private func generateStarsReaction() -> AvailableReactions.Reaction { + return AvailableReactions.Reaction( + isEnabled: false, + isPremium: false, + value: .stars, + title: "Star", + staticIcon: generateStarsReactionFile(kind: 0, isAnimatedSticker: true), + appearAnimation: generateStarsReactionFile(kind: 1, isAnimatedSticker: true), + selectAnimation: generateStarsReactionFile(kind: 2, isAnimatedSticker: true), + activateAnimation: generateStarsReactionFile(kind: 3, isAnimatedSticker: true), + effectAnimation: generateStarsReactionFile(kind: 4, isAnimatedSticker: true), + aroundAnimation: generateStarsReactionFile(kind: 5, isAnimatedSticker: true), + centerAnimation: generateStarsReactionFile(kind: 6, isAnimatedSticker: true) + ) +} + public final class AvailableReactions: Equatable, Codable { public final class Reaction: Equatable, Codable { private enum CodingKeys: String, CodingKey { @@ -17,6 +56,7 @@ public final class AvailableReactions: Equatable, Codable { case effectAnimation case aroundAnimation case centerAnimation + case isStars } public let isEnabled: Bool @@ -100,7 +140,12 @@ public final class AvailableReactions: Equatable, Codable { self.isEnabled = try container.decode(Bool.self, forKey: .isEnabled) self.isPremium = try container.decodeIfPresent(Bool.self, forKey: .isPremium) ?? false - self.value = .builtin(try container.decode(String.self, forKey: .value)) + let isStars = try container.decodeIfPresent(Bool.self, forKey: .isStars) ?? false + if isStars { + self.value = .stars + } else { + self.value = .builtin(try container.decode(String.self, forKey: .value)) + } self.title = try container.decode(String.self, forKey: .title) let staticIconData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .staticIcon) @@ -142,6 +187,8 @@ public final class AvailableReactions: Equatable, Codable { try container.encode(value, forKey: .value) case .custom: break + case .stars: + try container.encode(true, forKey: .isStars) } try container.encode(self.title, forKey: .title) @@ -172,6 +219,11 @@ public final class AvailableReactions: Equatable, Codable { reactions: [Reaction] ) { self.hash = hash + + var reactions = reactions + reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) + //TODO:release + reactions.append(generateStarsReaction()) self.reactions = reactions } @@ -189,7 +241,12 @@ public final class AvailableReactions: Equatable, Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.hash = try container.decodeIfPresent(Int32.self, forKey: .newHash) ?? 0 - self.reactions = try container.decode([Reaction].self, forKey: .reactions) + + //TODO:release + var reactions = try container.decode([Reaction].self, forKey: .reactions) + reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) + reactions.append(generateStarsReaction()) + self.reactions = reactions } public func encode(to encoder: Encoder) throws { @@ -268,6 +325,31 @@ func _internal_setCachedAvailableReactions(transaction: Transaction, availableRe } func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) -> Signal { + let starsReaction = generateStarsReaction() + let mapping: [String: KeyPath] = [ + "star_reaction_activate.tgs": \.activateAnimation, + "star_reaction_appear.tgs": \.appearAnimation, + "star_reaction_effect.tgs": \.effectAnimation, + "star_reaction_select.tgs": \.selectAnimation, + "star_reaction_static_icon.webp": \.staticIcon + ] + let optionalMapping: [String: KeyPath] = [ + "star_reaction_center.tgs": \.centerAnimation, + "star_reaction_effect.tgs": \.aroundAnimation + ] + for (key, path) in mapping { + if let filePath = Bundle.main.path(forResource: key, ofType: nil), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) { + postbox.mediaBox.storeResourceData(starsReaction[keyPath: path].resource.id, data: data) + } + } + for (key, path) in optionalMapping { + if let filePath = Bundle.main.path(forResource: key, ofType: nil), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) { + if let file = starsReaction[keyPath: path] { + postbox.mediaBox.storeResourceData(file.resource.id, data: data) + } + } + } + let poll = Signal { subscriber in let signal: Signal = _internal_cachedAvailableReactions(postbox: postbox) |> mapToSignal { current in @@ -281,6 +363,7 @@ func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) -> guard let result = result else { return .complete() } + switch result { case let .availableReactions(hash, reactions): let availableReactions = AvailableReactions( diff --git a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift index ace5e77447a..ba4ec8405ce 100644 --- a/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift +++ b/submodules/TelegramCore/Sources/State/ManagedConsumePersonalMessagesActions.swift @@ -656,6 +656,8 @@ func synchronizeSavedMessageTags(postbox: Postbox, network: Network, peerId: Pee reactionId = UInt64(bitPattern: id) case let .builtin(string): reactionId = md5StringHash(string) + case .stars: + reactionId = md5StringHash("star") } var titleId: UInt64? diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index bac8b35cff1..8e48ce953d6 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -411,6 +411,8 @@ func managedRecentReactions(postbox: Postbox, network: Network) -> Signal Signal map { files -> [OrderedItemListEntry] in @@ -442,6 +446,8 @@ func managedRecentReactions(postbox: Postbox, network: Network) -> Signal Signal Signal map { files -> [OrderedItemListEntry] in @@ -493,6 +503,8 @@ func managedTopReactions(postbox: Postbox, network: Network) -> Signal Signal Signal map { files -> [OrderedItemListEntry] in @@ -544,6 +560,8 @@ func managedDefaultTagReactions(postbox: Postbox, network: Network) -> Signal ignoreValues } +public func sendStarsReactionsInteractively(account: Account, messageId: MessageId, count: Int, isAnonymous: Bool) -> Signal { + return account.postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendStarsReaction, id: messageId, action: SendStarsReactionsAction(randomId: Int64.random(in: Int64.min ... Int64.max))) + transaction.updateMessage(messageId, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + var mappedCount = Int32(count) + var attributes = currentMessage.attributes + loop: for j in 0 ..< attributes.count { + if let current = attributes[j] as? PendingStarsReactionsMessageAttribute { + mappedCount += current.count + attributes.remove(at: j) + break loop + } + } + + attributes.append(PendingStarsReactionsMessageAttribute(accountPeerId: account.peerId, count: mappedCount, isAnonymous: isAnonymous)) + + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + |> ignoreValues +} + +func cancelPendingSendStarsReactionInteractively(account: Account, messageId: MessageId) -> Signal { + return account.postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendStarsReaction, id: messageId, action: nil) + transaction.updateMessage(messageId, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + var attributes = currentMessage.attributes + loop: for j in 0 ..< attributes.count { + if let _ = attributes[j] as? PendingStarsReactionsMessageAttribute { + attributes.remove(at: j) + break loop + } + } + + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + |> ignoreValues +} + +func _internal_forceSendPendingSendStarsReaction(account: Account, messageId: MessageId) -> Signal { + account.stateManager.forceSendPendingStarsReaction(messageId: messageId) + + return .complete() +} + +func _internal_updateStarsReactionIsAnonymous(account: Account, messageId: MessageId, isAnonymous: Bool) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + transaction.updateMessage(messageId, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + var attributes = currentMessage.attributes + for j in (0 ..< attributes.count).reversed() { + if let attribute = attributes[j] as? ReactionsMessageAttribute { + var updatedTopPeers = attribute.topPeers + if let index = updatedTopPeers.firstIndex(where: { $0.isMy }) { + updatedTopPeers[index].isAnonymous = isAnonymous + } + attributes[j] = ReactionsMessageAttribute(canViewList: attribute.canViewList, isTags: attribute.isTags, reactions: attribute.reactions, recentPeers: attribute.recentPeers, topPeers: updatedTopPeers) + } + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + + return transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) + } + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + + return account.network.request(Api.functions.messages.togglePaidReactionPrivacy(peer: inputPeer, msgId: messageId.id, private: isAnonymous ? .boolTrue : .boolFalse)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues + } +} + private enum RequestUpdateMessageReactionError { case generic } @@ -250,8 +348,88 @@ private func requestUpdateMessageReaction(postbox: Postbox, network: Network, st } } +private func requestSendStarsReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal { + return postbox.transaction { transaction -> (Peer, Int32, Bool)? in + guard let peer = transaction.getPeer(messageId.peerId) else { + return nil + } + guard let message = transaction.getMessage(messageId) else { + return nil + } + var count: Int32 = 0 + var isAnonymous = false + for attribute in message.attributes { + if let attribute = attribute as? PendingStarsReactionsMessageAttribute { + count += attribute.count + isAnonymous = attribute.isAnonymous + break + } + } + return (peer, count, isAnonymous) + } + |> castError(RequestUpdateMessageReactionError.self) + |> mapToSignal { peerAndValue in + guard let (peer, count, isAnonymous) = peerAndValue else { + return .fail(.generic) + } + guard let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + if messageId.namespace != Namespaces.Message.Cloud { + return .fail(.generic) + } + + if count > 0 { + let randomPartId = UInt64(UInt32(bitPattern: Int32.random(in: Int32.min ... Int32.max))) + let timestampPart = UInt64(UInt32(bitPattern: Int32(Date().timeIntervalSince1970))) + let randomId = (timestampPart << 32) | randomPartId + + var flags: Int32 = 0 + if isAnonymous { + flags |= 1 << 0 + } + + let signal: Signal = network.request(Api.functions.messages.sendPaidReaction(flags: flags, peer: inputPeer, msgId: messageId.id, count: count, randomId: Int64(bitPattern: randomId))) + |> mapError { _ -> RequestUpdateMessageReactionError in + return .generic + } + |> mapToSignal { result -> Signal in + stateManager.starsContext?.add(balance: Int64(-count), addTransaction: false) + //stateManager.starsContext?.load(force: true) + + return postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendStarsReaction, id: messageId, action: UpdateMessageReactionsAction()) + transaction.updateMessage(messageId, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + let reactions = mergedMessageReactions(attributes: currentMessage.attributes, isTags: currentMessage.areReactionsTags(accountPeerId: stateManager.accountPeerId)) + var attributes = currentMessage.attributes + for j in (0 ..< attributes.count).reversed() { + if attributes[j] is PendingStarsReactionsMessageAttribute || attributes[j] is ReactionsMessageAttribute { + attributes.remove(at: j) + } + } + if let reactions { + attributes.append(reactions) + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + stateManager.addUpdates(result) + } + |> castError(RequestUpdateMessageReactionError.self) + |> ignoreValues + } + return signal + } else { + return .complete() + } + } +} + private final class ManagedApplyPendingMessageReactionsActionsHelper { - var operationDisposables: [MessageId: Disposable] = [:] + var operationDisposables: [MessageId: (PendingMessageActionData, Disposable)] = [:] func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) { var disposeOperations: [Disposable] = [] @@ -260,23 +438,26 @@ private final class ManagedApplyPendingMessageReactionsActionsHelper { var hasRunningOperationForPeerId = Set() var validIds = Set() for entry in entries { + if let current = self.operationDisposables[entry.id], !current.0.isEqual(to: entry.action) { + self.operationDisposables.removeValue(forKey: entry.id) + disposeOperations.append(current.1) + } + if !hasRunningOperationForPeerId.contains(entry.id.peerId) { hasRunningOperationForPeerId.insert(entry.id.peerId) validIds.insert(entry.id) - if self.operationDisposables[entry.id] == nil { - let disposable = MetaDisposable() - beginOperations.append((entry, disposable)) - self.operationDisposables[entry.id] = disposable - } + let disposable = MetaDisposable() + beginOperations.append((entry, disposable)) + self.operationDisposables[entry.id] = (entry.action, disposable) } } var removeMergedIds: [MessageId] = [] - for (id, disposable) in self.operationDisposables { + for (id, actionAndDisposable) in self.operationDisposables { if !validIds.contains(id) { removeMergedIds.append(id) - disposeOperations.append(disposable) + disposeOperations.append(actionAndDisposable.1) } } @@ -288,13 +469,13 @@ private final class ManagedApplyPendingMessageReactionsActionsHelper { } func reset() -> [Disposable] { - let disposables = Array(self.operationDisposables.values) + let disposables = Array(self.operationDisposables.values.map(\.1)) self.operationDisposables.removeAll() return disposables } } -private func withTakenAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal) -> Signal { +private func withTakenReactionsAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal) -> Signal { return postbox.transaction { transaction -> Signal in var result: PendingMessageActionsEntry? @@ -307,6 +488,19 @@ private func withTakenAction(postbox: Postbox, type: PendingMessageActionType, i |> switchToLatest } +private func withTakenStarsAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal) -> Signal { + return postbox.transaction { transaction -> Signal in + var result: PendingMessageActionsEntry? + + if let action = transaction.getPendingMessageAction(type: type, id: id) as? SendStarsReactionsAction { + result = PendingMessageActionsEntry(id: id, action: action) + } + + return f(transaction, result) + } + |> switchToLatest +} + func managedApplyPendingMessageReactionsActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { return Signal { _ in let helper = Atomic(value: ManagedApplyPendingMessageReactionsActionsHelper()) @@ -327,7 +521,7 @@ func managedApplyPendingMessageReactionsActions(postbox: Postbox, network: Netwo } for (entry, disposable) in beginOperations { - let signal = withTakenAction(postbox: postbox, type: .updateReaction, id: entry.id, { transaction, entry -> Signal in + let signal = withTakenReactionsAction(postbox: postbox, type: .updateReaction, id: entry.id, { transaction, entry -> Signal in if let entry = entry { if let _ = entry.action as? UpdateMessageReactionsAction { return synchronizeMessageReactions(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id) @@ -360,6 +554,72 @@ func managedApplyPendingMessageReactionsActions(postbox: Postbox, network: Netwo } } +func managedApplyPendingMessageStarsReactionsActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedApplyPendingMessageReactionsActionsHelper()) + + let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendStarsReaction) + let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in + var entries: [PendingMessageActionsEntry] = [] + if let v = view.views[actionsKey] as? PendingMessageActionsView { + entries = v.entries + } + + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in + return helper.update(entries: entries) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let signal = withTakenStarsAction(postbox: postbox, type: .sendStarsReaction, id: entry.id, { transaction, entry -> Signal in + if let entry = entry { + if let _ = entry.action as? SendStarsReactionsAction { + let triggerSignal: Signal = stateManager.forceSendPendingStarsReaction + |> filter { + $0 == entry.id + } + |> map { _ -> Void in + return Void() + } + |> take(1) + |> timeout(5.0, queue: .mainQueue(), alternate: .single(Void())) + + return triggerSignal + |> mapToSignal { _ -> Signal in + return synchronizeMessageStarsReactions(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id) + } + } else { + assertionFailure() + } + } + return .complete() + }) + |> then( + postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendStarsReaction, id: entry.id, action: nil) + } + |> ignoreValues + ) + + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + disposable.dispose() + } + } +} + private func synchronizeMessageReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal { return requestUpdateMessageReaction(postbox: postbox, network: network, stateManager: stateManager, messageId: id) |> `catch` { _ -> Signal in @@ -384,6 +644,30 @@ private func synchronizeMessageReactions(transaction: Transaction, postbox: Post } } +private func synchronizeMessageStarsReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal { + return requestSendStarsReaction(postbox: postbox, network: network, stateManager: stateManager, messageId: id) + |> `catch` { _ -> Signal in + return postbox.transaction { transaction -> Void in + transaction.setPendingMessageAction(type: .sendStarsReaction, id: id, action: nil) + transaction.updateMessage(id, update: { currentMessage in + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + var attributes = currentMessage.attributes + loop: for j in 0 ..< attributes.count { + if let _ = attributes[j] as? PendingStarsReactionsMessageAttribute { + attributes.remove(at: j) + break loop + } + } + return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + |> ignoreValues + } +} + public extension EngineMessageReactionListContext.State { init(message: EngineMessage, readStats: MessageReadStats?, reaction: MessageReaction.Reaction?) { var totalCount = 0 @@ -682,7 +966,13 @@ func _internal_updatePeerReactionSettings(account: Account, peerId: PeerId, reac reactionLimitValue = maxReactionCount } - return account.network.request(Api.functions.messages.setChatAvailableReactions(flags: flags, peer: inputPeer, availableReactions: mappedReactions, reactionsLimit: reactionLimitValue)) + var paidEnabled: Api.Bool? + if let starsAllowed = reactionSettings.starsAllowed { + flags |= 1 << 1 + paidEnabled = starsAllowed ? .boolTrue : .boolFalse + } + + return account.network.request(Api.functions.messages.setChatAvailableReactions(flags: flags, peer: inputPeer, availableReactions: mappedReactions, reactionsLimit: reactionLimitValue, paidEnabled: paidEnabled)) |> map(Optional.init) |> `catch` { error -> Signal in if error.errorDescription == "CHAT_NOT_MODIFIED" { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 9bbff0d1df7..9ca64a5e9e9 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 185 + return 186 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 0ec47fcdf35..79f03b83089 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -182,7 +182,7 @@ extension Api.Chat { return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) case let .chatForbidden(id, _): return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)) - case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) case let .channelForbidden(_, id, _, _, _): return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index 3e00ee7a835..ba765fa57e1 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -16,6 +16,7 @@ public enum ServerProvidedSuggestion: String { case setupBirthday = "BIRTHDAY_SETUP" case todayBirthdays = "BIRTHDAY_CONTACTS_TODAY" case gracePremium = "PREMIUM_GRACE" + case starsSubscriptionLowBalance = "STARS_SUBSCRIPTION_LOW_BALANCE" } private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift index b5f8795130f..cadd5f91bdb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_BotInfo.swift @@ -51,13 +51,15 @@ public final class BotInfo: PostboxCoding, Equatable { public let video: TelegramMediaFile? public let commands: [BotCommand] public let menuButton: BotMenuButton + public let privacyPolicyUrl: String? - public init(description: String, photo: TelegramMediaImage?, video: TelegramMediaFile?, commands: [BotCommand], menuButton: BotMenuButton) { + public init(description: String, photo: TelegramMediaImage?, video: TelegramMediaFile?, commands: [BotCommand], menuButton: BotMenuButton, privacyPolicyUrl: String?) { self.description = description self.photo = photo self.video = video self.commands = commands self.menuButton = menuButton + self.privacyPolicyUrl = privacyPolicyUrl } public init(decoder: PostboxDecoder) { @@ -74,6 +76,7 @@ public final class BotInfo: PostboxCoding, Equatable { } self.commands = decoder.decodeObjectArrayWithDecoderForKey("c") self.menuButton = (decoder.decodeObjectForKey("b", decoder: { BotMenuButton(decoder: $0) }) as? BotMenuButton) ?? .commands + self.privacyPolicyUrl = decoder.decodeOptionalStringForKey("pp") } public func encode(_ encoder: PostboxEncoder) { @@ -90,9 +93,14 @@ public final class BotInfo: PostboxCoding, Equatable { } encoder.encodeObjectArray(self.commands, forKey: "c") encoder.encodeObject(self.menuButton, forKey: "b") + if let privacyPolicyUrl = self.privacyPolicyUrl { + encoder.encodeString(privacyPolicyUrl, forKey: "pp") + } else { + encoder.encodeNil(forKey: "pp") + } } public static func ==(lhs: BotInfo, rhs: BotInfo) -> Bool { - return lhs.description == rhs.description && lhs.commands == rhs.commands && lhs.menuButton == rhs.menuButton && lhs.photo == rhs.photo + return lhs.description == rhs.description && lhs.commands == rhs.commands && lhs.menuButton == rhs.menuButton && lhs.photo == rhs.photo && lhs.privacyPolicyUrl == rhs.privacyPolicyUrl } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index e3e2bd689b0..7a81baa2605 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -636,10 +636,10 @@ public final class CachedChannelData: CachedPeerData { self.reactionSettings = .known(reactionSettings) } else if let legacyAllowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") { let allowedReactions: PeerAllowedReactions = .limited(legacyAllowedReactions.map(MessageReaction.Reaction.builtin)) - self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil)) + self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil, starsAllowed: nil)) } else if let allowedReactions = decoder.decode(PeerAllowedReactions.self, forKey: "allowedReactionSet") { let allowedReactions = allowedReactions - self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil)) + self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil, starsAllowed: nil)) } else { self.reactionSettings = .unknown } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift index 2125bc1a596..544760a6b30 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedGroupData.swift @@ -100,10 +100,12 @@ extension PeerAllowedReactions { public final class PeerReactionSettings: Equatable, Codable { public let allowedReactions: PeerAllowedReactions public let maxReactionCount: Int32? + public let starsAllowed: Bool? - public init(allowedReactions: PeerAllowedReactions, maxReactionCount: Int32?) { + public init(allowedReactions: PeerAllowedReactions, maxReactionCount: Int32?, starsAllowed: Bool?) { self.allowedReactions = allowedReactions self.maxReactionCount = maxReactionCount + self.starsAllowed = starsAllowed } public static func ==(lhs: PeerReactionSettings, rhs: PeerReactionSettings) -> Bool { @@ -116,6 +118,9 @@ public final class PeerReactionSettings: Equatable, Codable { if lhs.maxReactionCount != rhs.maxReactionCount { return false } + if lhs.starsAllowed != rhs.starsAllowed { + return false + } return true } } @@ -266,10 +271,10 @@ public final class CachedGroupData: CachedPeerData { self.reactionSettings = .known(reactionSettings) } else if let legacyAllowedReactions = decoder.decodeOptionalStringArrayForKey("allowedReactions") { let allowedReactions: PeerAllowedReactions = .limited(legacyAllowedReactions.map(MessageReaction.Reaction.builtin)) - self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil)) + self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil, starsAllowed: nil)) } else if let allowedReactions = decoder.decode(PeerAllowedReactions.self, forKey: "allowedReactionSet") { let allowedReactions = allowedReactions - self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil)) + self.reactionSettings = .known(PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: nil, starsAllowed: nil)) } else { self.reactionSettings = .unknown } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift index f10d5c7861f..6c61a31ba40 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ExportedInvitation.swift @@ -1,7 +1,7 @@ import Postbox public enum ExportedInvitation: Codable, Equatable { - case link(link: String, title: String?, isPermanent: Bool, requestApproval: Bool, isRevoked: Bool, adminId: PeerId, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, count: Int32?, requestedCount: Int32?) + case link(link: String, title: String?, isPermanent: Bool, requestApproval: Bool, isRevoked: Bool, adminId: PeerId, date: Int32, startDate: Int32?, expireDate: Int32?, usageLimit: Int32?, count: Int32?, requestedCount: Int32?, pricing: StarsSubscriptionPricing?) case publicJoinRequest public init(from decoder: Decoder) throws { @@ -21,8 +21,9 @@ public enum ExportedInvitation: Codable, Equatable { let usageLimit = try container.decodeIfPresent(Int32.self, forKey: "usageLimit") let count = try container.decodeIfPresent(Int32.self, forKey: "count") let requestedCount = try? container.decodeIfPresent(Int32.self, forKey: "requestedCount") + let pricing = try? container.decodeIfPresent(StarsSubscriptionPricing.self, forKey: "pricing") - self = .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount) + self = .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount, pricing: pricing) } else { self = .publicJoinRequest } @@ -32,7 +33,7 @@ public enum ExportedInvitation: Codable, Equatable { var container = encoder.container(keyedBy: StringCodingKey.self) switch self { - case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): + case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing): let type: Int32 = 0 try container.encode(type, forKey: "t") try container.encode(link, forKey: "l") @@ -47,6 +48,7 @@ public enum ExportedInvitation: Codable, Equatable { try container.encodeIfPresent(usageLimit, forKey: "usageLimit") try container.encodeIfPresent(count, forKey: "count") try container.encodeIfPresent(requestedCount, forKey: "requestedCount") + try container.encodeIfPresent(pricing, forKey: "pricing") case .publicJoinRequest: let type: Int32 = 1 try container.encode(type, forKey: "t") @@ -55,8 +57,8 @@ public enum ExportedInvitation: Codable, Equatable { public static func ==(lhs: ExportedInvitation, rhs: ExportedInvitation) -> Bool { switch lhs { - case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): - if case .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount) = rhs { + case let .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing): + if case .link(link, title, isPermanent, requestApproval, isRevoked, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing) = rhs { return true } else { return false @@ -72,8 +74,8 @@ public enum ExportedInvitation: Codable, Equatable { public func withUpdated(isRevoked: Bool) -> ExportedInvitation { switch self { - case let .link(link, title, isPermanent, requestApproval, _, adminId, date, startDate, expireDate, usageLimit, count, requestedCount): - return .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount) + case let .link(link, title, isPermanent, requestApproval, _, adminId, date, startDate, expireDate, usageLimit, count, requestedCount, pricing): + return .link(link: link, title: title, isPermanent: isPermanent, requestApproval: requestApproval, isRevoked: isRevoked, adminId: adminId, date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: count, requestedCount: requestedCount, pricing: pricing) case .publicJoinRequest: return .publicJoinRequest } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 928ba32a69f..a9bde8238dc 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -184,6 +184,7 @@ public extension PendingMessageActionType { static let updateReaction = PendingMessageActionType(rawValue: 1) static let sendScheduledMessageImmediately = PendingMessageActionType(rawValue: 2) static let readReaction = PendingMessageActionType(rawValue: 3) + static let sendStarsReaction = PendingMessageActionType(rawValue: 4) } public let peerIdNamespacesWithInitialCloudMessageHoles = [Namespaces.Peer.CloudUser, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel] diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessRestrictionInfo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessRestrictionInfo.swift index 940640e28f2..509aa85c8df 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessRestrictionInfo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessRestrictionInfo.swift @@ -11,6 +11,12 @@ public final class RestrictionRule: PostboxCoding, Equatable { self.text = text } + public init(platform: String) { + self.platform = platform + self.reason = "" + self.text = "" + } + public init(decoder: PostboxDecoder) { self.platform = decoder.decodeStringForKey("p", orElse: "all") self.reason = decoder.decodeStringForKey("r", orElse: "") diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift index 6758c551202..5b0fa01ee05 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_ReactionsMessageAttribute.swift @@ -12,12 +12,15 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { public enum Reaction: Hashable, Comparable, Codable, PostboxCoding { case builtin(String) case custom(Int64) + case stars public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) if let value = try container.decodeIfPresent(String.self, forKey: "v") { self = .builtin(value) + } else if let _ = try container.decodeIfPresent(Int64.self, forKey: "star") { + self = .stars } else { self = .custom(try container.decode(Int64.self, forKey: "cfid")) } @@ -26,6 +29,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { public init(decoder: PostboxDecoder) { if let value = decoder.decodeOptionalStringForKey("v") { self = .builtin(value) + } else if let _ = decoder.decodeOptionalInt64ForKey("star") { + self = .stars } else { self = .custom(decoder.decodeInt64ForKey("cfid", orElse: 0)) } @@ -39,6 +44,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { try container.encode(value, forKey: "v") case let .custom(fileId): try container.encode(fileId, forKey: "cfid") + case .stars: + try container.encode(0 as Int64, forKey: "star") } } @@ -48,6 +55,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { encoder.encodeString(value, forKey: "v") case let .custom(fileId): encoder.encodeInt64(fileId, forKey: "cfid") + case .stars: + encoder.encodeInt64(0, forKey: "star") } } @@ -59,6 +68,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { return lhsValue < rhsValue case .custom: return true + case .stars: + return false } case let .custom(lhsValue): switch rhs { @@ -66,6 +77,17 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { return false case let .custom(rhsValue): return lhsValue < rhsValue + case .stars: + return false + } + case .stars: + switch rhs { + case .builtin: + return true + case .custom: + return true + case .stars: + return false } } } @@ -88,6 +110,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { public init(decoder: PostboxDecoder) { if let value = decoder.decodeOptionalStringForKey("v") { self.value = .builtin(value) + } else if let _ = decoder.decodeOptionalInt64ForKey("star") { + self.value = .stars } else { self.value = .custom(decoder.decodeInt64ForKey("cfid", orElse: 0)) } @@ -106,6 +130,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { if let value = try container.decodeIfPresent(String.self, forKey: "v") { self.value = .builtin(value) + } else if let _ = try container.decodeIfPresent(Int64.self, forKey: "star") { + self.value = .stars } else { self.value = .custom(try container.decode(Int64.self, forKey: "cfid")) } @@ -125,6 +151,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { encoder.encodeString(value, forKey: "v") case let .custom(fileId): encoder.encodeInt64(fileId, forKey: "cfid") + case .stars: + encoder.encodeInt64(0, forKey: "star") } encoder.encodeInt32(self.count, forKey: "c") if let chosenOrder = self.chosenOrder { @@ -142,6 +170,8 @@ public struct MessageReaction: Equatable, PostboxCoding, Codable { try container.encode(value, forKey: "v") case let .custom(fileId): try container.encode(fileId, forKey: "cfid") + case .stars: + try container.encode(0 as Int64, forKey: "star") } try container.encode(self.count, forKey: "c") try container.encodeIfPresent(self.chosenOrder.flatMap(Int32.init), forKey: "cord") @@ -157,6 +187,8 @@ extension MessageReaction.Reaction { self = .builtin(emoticon) case let .reactionCustomEmoji(documentId): self = .custom(documentId) + case .reactionPaid: + self = .stars } } @@ -166,6 +198,8 @@ extension MessageReaction.Reaction { return .reactionEmoji(emoticon: value) case let .custom(fileId): return .reactionCustomEmoji(documentId: fileId) + case .stars: + return .reactionPaid } } } @@ -191,6 +225,9 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { var typeId: UInt8 = 1 buffer.write(&typeId, offset: 0, length: 1) buffer.write(&fileId, offset: 0, length: 8) + case .stars: + var typeId: UInt8 = 2 + buffer.write(&typeId, offset: 0, length: 1) } return buffer @@ -231,6 +268,8 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { var fileId: Int64 = 0 readBuffer.read(&fileId, offset: 0, length: 8) return .custom(fileId) + case 2: + return .stars default: return nil } @@ -256,6 +295,8 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { public init(decoder: PostboxDecoder) { if let value = decoder.decodeOptionalStringForKey("v") { self.value = .builtin(value) + } else if let _ = decoder.decodeOptionalInt64ForKey("star") { + self.value = .stars } else { self.value = .custom(decoder.decodeInt64ForKey("cfid", orElse: 0)) } @@ -272,6 +313,8 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { encoder.encodeString(value, forKey: "v") case let .custom(fileId): encoder.encodeInt64(fileId, forKey: "cfid") + case .stars: + encoder.encodeInt64(0, forKey: "star") } encoder.encodeInt32(self.isLarge ? 1 : 0, forKey: "l") encoder.encodeInt32(self.isUnseen ? 1 : 0, forKey: "u") @@ -285,10 +328,51 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { } } + public struct TopPeer: Equatable, PostboxCoding { + public var peerId: PeerId? + public var count: Int32 + public var isTop: Bool + public var isMy: Bool + public var isAnonymous: Bool + + public init(peerId: PeerId?, count: Int32, isTop: Bool, isMy: Bool, isAnonymous: Bool) { + self.peerId = peerId + self.count = count + self.isMy = isMy + self.isTop = isTop + self.isAnonymous = isAnonymous + } + + public init(decoder: PostboxDecoder) { + if let peerId = decoder.decodeOptionalInt64ForKey("p") { + self.peerId = PeerId(peerId) + } else { + self.peerId = nil + } + self.count = decoder.decodeInt32ForKey("c", orElse: 0) + self.isTop = decoder.decodeBoolForKey("t", orElse: false) + self.isMy = decoder.decodeBoolForKey("m", orElse: false) + self.isAnonymous = decoder.decodeBoolForKey("anon", orElse: false) + } + + public func encode(_ encoder: PostboxEncoder) { + if let peerId = self.peerId { + encoder.encodeInt64(peerId.toInt64(), forKey: "p") + } else { + encoder.encodeNil(forKey: "p") + } + encoder.encodeInt32(self.count, forKey: "c") + encoder.encodeBool(self.isTop, forKey: "t") + encoder.encodeBool(self.isMy, forKey: "m") + encoder.encodeBool(self.isAnonymous, forKey: "anon") + } + } + public let canViewList: Bool public let isTags: Bool public let reactions: [MessageReaction] public let recentPeers: [RecentPeer] + public let topPeers: [TopPeer] public var associatedPeerIds: [PeerId] { return self.recentPeers.map(\.peerId) @@ -306,17 +390,20 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { if !result.contains(mediaId) { result.append(mediaId) } + case .stars: + break } } return result } - public init(canViewList: Bool, isTags: Bool, reactions: [MessageReaction], recentPeers: [RecentPeer]) { + public init(canViewList: Bool, isTags: Bool, reactions: [MessageReaction], recentPeers: [RecentPeer], topPeers: [TopPeer]) { self.canViewList = canViewList self.isTags = isTags self.reactions = reactions self.recentPeers = recentPeers + self.topPeers = topPeers } required public init(decoder: PostboxDecoder) { @@ -324,6 +411,7 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { self.isTags = decoder.decodeBoolForKey("tg", orElse: false) self.reactions = decoder.decodeObjectArrayWithDecoderForKey("r") self.recentPeers = decoder.decodeObjectArrayWithDecoderForKey("rp") + self.topPeers = decoder.decodeObjectArrayWithDecoderForKey("tp") } public func encode(_ encoder: PostboxEncoder) { @@ -331,6 +419,7 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { encoder.encodeBool(self.isTags, forKey: "tg") encoder.encodeObjectArray(self.reactions, forKey: "r") encoder.encodeObjectArray(self.recentPeers, forKey: "rp") + encoder.encodeObjectArray(self.topPeers, forKey: "tp") } public static func ==(lhs: ReactionsMessageAttribute, rhs: ReactionsMessageAttribute) -> Bool { @@ -346,6 +435,9 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { if lhs.recentPeers != rhs.recentPeers { return false } + if lhs.topPeers != rhs.topPeers { + return false + } return true } @@ -367,7 +459,8 @@ public final class ReactionsMessageAttribute: Equatable, MessageAttribute { var recentPeer = recentPeer recentPeer.isUnseen = false return recentPeer - } + }, + topPeers: self.topPeers ) } } @@ -430,6 +523,8 @@ public final class PendingReactionsMessageAttribute: MessageAttribute { if !result.contains(mediaId) { result.append(mediaId) } + case .stars: + break } } @@ -466,3 +561,39 @@ public final class PendingReactionsMessageAttribute: MessageAttribute { encoder.encodeBool(self.isTags, forKey: "itag") } } + +public final class PendingStarsReactionsMessageAttribute: MessageAttribute { + public let accountPeerId: PeerId? + public let count: Int32 + public let isAnonymous: Bool + + public var associatedPeerIds: [PeerId] { + var peerIds: [PeerId] = [] + if let accountPeerId = self.accountPeerId { + peerIds.append(accountPeerId) + } + return peerIds + } + + public init(accountPeerId: PeerId?, count: Int32, isAnonymous: Bool) { + self.accountPeerId = accountPeerId + self.count = count + self.isAnonymous = isAnonymous + } + + required public init(decoder: PostboxDecoder) { + self.accountPeerId = decoder.decodeOptionalInt64ForKey("ap").flatMap(PeerId.init) + self.count = decoder.decodeInt32ForKey("cnt", orElse: 1) + self.isAnonymous = decoder.decodeBoolForKey("anon", orElse: false) + } + + public func encode(_ encoder: PostboxEncoder) { + if let accountPeerId = self.accountPeerId { + encoder.encodeInt64(accountPeerId.toInt64(), forKey: "ap") + } else { + encoder.encodeNil(forKey: "ap") + } + encoder.encodeInt32(self.count, forKey: "cnt") + encoder.encodeBool(self.isAnonymous, forKey: "anon") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift index 642ac821c3b..c8a0ad4f8c0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift @@ -154,9 +154,10 @@ public final class RecentEmojiItem: Codable, Equatable { } public struct RecentReactionItemId { - public enum Id : Hashable { + public enum Id: Hashable { case custom(MediaId) case builtin(String) + case stars } public let rawValue: MemoryBuffer @@ -184,6 +185,8 @@ public struct RecentReactionItemId { assert(rawValue.length >= 1 + 2 + Int(length)) self.id = .builtin(String(data: Data(bytes: rawValue.memory.advanced(by: 1 + 2), count: Int(length)), encoding: .utf8) ?? ".") + } else if type == 2 { + self.id = .stars } else { assert(false) self.id = .builtin(".") @@ -216,12 +219,23 @@ public struct RecentReactionItemId { let _ = memcpy(self.rawValue.memory.advanced(by: 1 + 2), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), bytes.count) } } + + public init(_ id: Id) { + precondition(id == .stars) + self.id = id + + self.rawValue = MemoryBuffer(memory: malloc(1)!, capacity: 1, length: 1, freeWhenDone: true) + + var type: UInt8 = 2 + memcpy(self.rawValue.memory.advanced(by: 0), &type, 1) + } } public final class RecentReactionItem: Codable, Equatable { public enum Content: Equatable { case custom(TelegramMediaFile) case builtin(String) + case stars } public let content: Content @@ -232,6 +246,8 @@ public final class RecentReactionItem: Codable, Equatable { return RecentReactionItemId(value) case let .custom(file): return RecentReactionItemId(file.fileId) + case .stars: + return RecentReactionItemId(.stars) } } @@ -244,6 +260,8 @@ public final class RecentReactionItem: Codable, Equatable { if let mediaData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: "m") { self.content = .custom(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: mediaData.data)))) + } else if let _ = try container.decodeIfPresent(Int64.self, forKey: "star") { + self.content = .stars } else { self.content = .builtin(try container.decode(String.self, forKey: "s")) } @@ -257,6 +275,8 @@ public final class RecentReactionItem: Codable, Equatable { try container.encode(PostboxEncoder().encodeObjectToRawData(file), forKey: "m") case let .builtin(string): try container.encode(string, forKey: "s") + case .stars: + try container.encode(0 as Int64, forKey: "star") } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 717c902f4ef..d20dab8bcf3 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -43,6 +43,7 @@ public struct TelegramChannelBroadcastFlags: OptionSet { public static let messagesShouldHaveSignatures = TelegramChannelBroadcastFlags(rawValue: 1 << 0) public static let hasDiscussionGroup = TelegramChannelBroadcastFlags(rawValue: 1 << 1) + public static let messagesShouldHaveProfiles = TelegramChannelBroadcastFlags(rawValue: 1 << 2) } public struct TelegramChannelBroadcastInfo: Equatable { @@ -173,6 +174,7 @@ public final class TelegramChannel: Peer, Equatable { public let profileBackgroundEmojiId: Int64? public let emojiStatus: PeerEmojiStatus? public let approximateBoostLevel: Int32? + public let subscriptionUntilDate: Int32? public var indexName: PeerIndexNameRepresentation { var addressNames = self.usernames.map { $0.username } @@ -238,7 +240,8 @@ public final class TelegramChannel: Peer, Equatable { profileColor: PeerNameColor?, profileBackgroundEmojiId: Int64?, emojiStatus: PeerEmojiStatus?, - approximateBoostLevel: Int32? + approximateBoostLevel: Int32?, + subscriptionUntilDate: Int32? ) { self.id = id self.accessHash = accessHash @@ -262,6 +265,7 @@ public final class TelegramChannel: Peer, Equatable { self.profileBackgroundEmojiId = profileBackgroundEmojiId self.emojiStatus = emojiStatus self.approximateBoostLevel = approximateBoostLevel + self.subscriptionUntilDate = subscriptionUntilDate } public init(decoder: PostboxDecoder) { @@ -297,6 +301,7 @@ public final class TelegramChannel: Peer, Equatable { self.profileBackgroundEmojiId = decoder.decodeOptionalInt64ForKey("pgem") self.emojiStatus = decoder.decode(PeerEmojiStatus.self, forKey: "emjs") self.approximateBoostLevel = decoder.decodeOptionalInt32ForKey("abl") + self.subscriptionUntilDate = decoder.decodeOptionalInt32ForKey("sud") } public func encode(_ encoder: PostboxEncoder) { @@ -388,6 +393,12 @@ public final class TelegramChannel: Peer, Equatable { } else { encoder.encodeNil(forKey: "abl") } + + if let subscriptionUntilDate = self.subscriptionUntilDate { + encoder.encodeInt32(subscriptionUntilDate, forKey: "sud") + } else { + encoder.encodeNil(forKey: "sud") + } } public func isEqual(_ other: Peer) -> Bool { @@ -446,51 +457,57 @@ public final class TelegramChannel: Peer, Equatable { if lhs.approximateBoostLevel != rhs.approximateBoostLevel { return false } - + if lhs.subscriptionUntilDate != rhs.subscriptionUntilDate { + return false + } return true } public func withUpdatedAddressName(_ addressName: String?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: addressName, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedAddressNames(_ addressNames: [TelegramPeerUsername]) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: addressNames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedDefaultBannedRights(_ defaultBannedRights: TelegramChatBannedRights?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedFlags(_ flags: TelegramChannelFlags) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedStoriesHidden(_ storiesHidden: Bool?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedNameColor(_ nameColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedBackgroundEmojiId(_ backgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedProfileColor(_ profileColor: PeerNameColor?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedProfileBackgroundEmojiId(_ profileBackgroundEmojiId: Int64?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: emojiStatus, approximateBoostLevel: self.approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) } public func withUpdatedApproximateBoostLevel(_ approximateBoostLevel: Int32?) -> TelegramChannel { - return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: approximateBoostLevel) + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: approximateBoostLevel, subscriptionUntilDate: self.subscriptionUntilDate) + } + + public func withUpdatedSubscriptionUntilDate(_ subscriptionUntilDate: Int32?) -> TelegramChannel { + return TelegramChannel(id: self.id, accessHash: self.accessHash, title: self.title, username: self.username, photo: self.photo, creationDate: self.creationDate, version: self.version, participationStatus: self.participationStatus, info: self.info, flags: self.flags, restrictionInfo: self.restrictionInfo, adminRights: self.adminRights, bannedRights: self.bannedRights, defaultBannedRights: self.defaultBannedRights, usernames: self.usernames, storiesHidden: self.storiesHidden, nameColor: self.nameColor, backgroundEmojiId: self.backgroundEmojiId, profileColor: self.profileColor, profileBackgroundEmojiId: self.profileBackgroundEmojiId, emojiStatus: self.emojiStatus, approximateBoostLevel: self.approximateBoostLevel, subscriptionUntilDate: subscriptionUntilDate) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UpdateMessageReactionsAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UpdateMessageReactionsAction.swift index 7a944f58bfe..f62f5ebbbd9 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UpdateMessageReactionsAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UpdateMessageReactionsAction.swift @@ -18,3 +18,30 @@ public final class UpdateMessageReactionsAction: PendingMessageActionData { } } } + +public final class SendStarsReactionsAction: PendingMessageActionData { + public let randomId: Int64 + + public init(randomId: Int64) { + self.randomId = randomId + } + + public init(decoder: PostboxDecoder) { + self.randomId = decoder.decodeInt64ForKey("id", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.randomId, forKey: "id") + } + + public func isEqual(to: PendingMessageActionData) -> Bool { + if let other = to as? SendStarsReactionsAction { + if self.randomId != other.randomId { + return false + } + return true + } else { + return false + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 1531576a483..fa18ad9478b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -2254,7 +2254,7 @@ func _internal_groupCallDisplayAsAvailablePeers(accountPeerId: PeerId, network: for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 4c6c5e094dd..b41f29d01ef 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -48,13 +48,13 @@ extension EnginePeerCachedInfoItem: Equatable where T: Equatable { public enum EngineChannelParticipant: Equatable { case creator(id: EnginePeer.Id, adminInfo: ChannelParticipantAdminInfo?, rank: String?) - case member(id: EnginePeer.Id, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?) + case member(id: EnginePeer.Id, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?, subscriptionUntilDate: Int32?) public var peerId: EnginePeer.Id { switch self { case let .creator(id, _, _): return id - case let .member(id, _, _, _, _): + case let .member(id, _, _, _, _, _): return id } } @@ -65,8 +65,8 @@ public extension EngineChannelParticipant { switch participant { case let .creator(id, adminInfo, rank): self = .creator(id: id, adminInfo: adminInfo, rank: rank) - case let .member(id, invitedAt, adminInfo, banInfo, rank): - self = .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank) + case let .member(id, invitedAt, adminInfo, banInfo, rank, subscriptionUntilDate): + self = .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } } @@ -74,8 +74,8 @@ public extension EngineChannelParticipant { switch self { case let .creator(id, adminInfo, rank): return .creator(id: id, adminInfo: adminInfo, rank: rank) - case let .member(id, invitedAt, adminInfo, banInfo, rank): - return .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank) + case let .member(id, invitedAt, adminInfo, banInfo, rank, subscriptionUntilDate): + return .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } } } @@ -2131,5 +2131,33 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct BotPrivacyPolicyUrl: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Optional + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.botInfo?.privacyPolicyUrl + } else { + return nil + } + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 4c5b712b090..c558333abcd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -12,6 +12,7 @@ private class AdMessagesHistoryContextImpl { case text case textEntities case media + case contentMedia case color case backgroundEmojiId case url @@ -32,6 +33,7 @@ private class AdMessagesHistoryContextImpl { public let text: String public let textEntities: [MessageTextEntity] public let media: [Media] + public let contentMedia: [Media] public let color: PeerNameColor? public let backgroundEmojiId: Int64? public let url: String @@ -47,6 +49,7 @@ private class AdMessagesHistoryContextImpl { text: String, textEntities: [MessageTextEntity], media: [Media], + contentMedia: [Media], color: PeerNameColor?, backgroundEmojiId: Int64?, url: String, @@ -61,6 +64,7 @@ private class AdMessagesHistoryContextImpl { self.text = text self.textEntities = textEntities self.media = media + self.contentMedia = contentMedia self.color = color self.backgroundEmojiId = backgroundEmojiId self.url = url @@ -89,6 +93,12 @@ private class AdMessagesHistoryContextImpl { self.media = mediaData.compactMap { data -> Media? in return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media } + + let contentMediaData = try container.decode([Data].self, forKey: .contentMedia) + self.contentMedia = contentMediaData.compactMap { data -> Media? in + return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media + } + self.color = try container.decodeIfPresent(Int32.self, forKey: .color).flatMap { PeerNameColor(rawValue: $0) } self.backgroundEmojiId = try container.decodeIfPresent(Int64.self, forKey: .backgroundEmojiId) @@ -116,6 +126,13 @@ private class AdMessagesHistoryContextImpl { return encoder.makeData() } try container.encode(mediaData, forKey: .media) + + let contentMediaData = self.contentMedia.map { media -> Data in + let encoder = PostboxEncoder() + encoder.encodeRootObject(media) + return encoder.makeData() + } + try container.encode(contentMediaData, forKey: .contentMedia) try container.encodeIfPresent(self.color?.rawValue, forKey: .color) try container.encodeIfPresent(self.backgroundEmojiId, forKey: .backgroundEmojiId) @@ -153,6 +170,14 @@ private class AdMessagesHistoryContextImpl { return false } } + if lhs.contentMedia.count != rhs.contentMedia.count { + return false + } + for i in 0 ..< lhs.contentMedia.count { + if !lhs.contentMedia[i].isEqual(to: rhs.contentMedia[i]) { + return false + } + } if lhs.url != rhs.url { return false } @@ -181,7 +206,7 @@ private class AdMessagesHistoryContextImpl { case .recommended: mappedMessageType = .recommended } - attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, url: self.url, buttonText: self.buttonText, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo, canReport: self.canReport)) + attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, messageType: mappedMessageType, url: self.url, buttonText: self.buttonText, sponsorInfo: self.sponsorInfo, additionalInfo: self.additionalInfo, canReport: self.canReport, hasContentMedia: !self.contentMedia.isEmpty)) if !self.textEntities.isEmpty { let attribute = TextEntitiesMessageAttribute(entities: self.textEntities) attributes.append(attribute) @@ -215,7 +240,8 @@ private class AdMessagesHistoryContextImpl { profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, - approximateBoostLevel: nil + approximateBoostLevel: nil, + subscriptionUntilDate: nil ) messagePeers[author.id] = author @@ -240,7 +266,7 @@ private class AdMessagesHistoryContextImpl { author: author, text: self.text, attributes: attributes, - media: self.media, + media: !self.contentMedia.isEmpty ? self.contentMedia : self.media, peers: messagePeers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], @@ -421,7 +447,7 @@ private class AdMessagesHistoryContextImpl { for message in messages { switch message { - case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, color, buttonText, sponsorInfo, additionalInfo): + case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, media, color, buttonText, sponsorInfo, additionalInfo): var parsedEntities: [MessageTextEntity] = [] if let entities = entities { parsedEntities = messageTextEntitiesFromApiEntities(entities) @@ -441,6 +467,8 @@ private class AdMessagesHistoryContextImpl { } let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) } + let (contentMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId) + parsedMessages.append(CachedMessage( opaqueId: randomId.makeData(), messageType: isRecommended ? .recommended : .sponsored, @@ -448,6 +476,7 @@ private class AdMessagesHistoryContextImpl { text: message, textEntities: parsedEntities, media: photo.flatMap { [$0] } ?? [], + contentMedia: contentMedia.flatMap { [$0] } ?? [], color: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, url: url, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index 136993ab0ea..523354bb0fc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -505,6 +505,8 @@ public final class EngineStoryViewListContext { return nil case let .custom(fileId): return transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile + case .stars: + return nil } } ))) @@ -696,6 +698,8 @@ public final class EngineStoryViewListContext { reactionFile = nil case let .custom(fileId): reactionFile = transaction.getMedia(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)) as? TelegramMediaFile + case .stars: + reactionFile = nil } items.append(.view(Item.View( peer: EnginePeer(peer), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index 0c247175432..d50eb68079b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -109,7 +109,13 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, return .single([]) } - if let channel = peer as? TelegramChannel, case .group = channel.info { + if let channel = peer as? TelegramChannel { + if case .group = channel.info { + } else if case let .broadcast(info) = channel.info { + if !info.flags.contains(.messagesShouldHaveProfiles) { + return .single([]) + } + } } else { return .single([]) } @@ -138,7 +144,7 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, for chat in chats { if let groupOrChannel = parsedPeers.get(chat.peerId) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 31c562b56d2..42735b6e07f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -345,7 +345,23 @@ public extension TelegramEngine { isLarge: false, storeAsRecentlyUsed: false, add: true - ).start() + ).startStandalone() + } + + public func sendStarsReaction(id: EngineMessage.Id, count: Int, isAnonymous: Bool) { + let _ = sendStarsReactionsInteractively(account: self.account, messageId: id, count: count, isAnonymous: isAnonymous).startStandalone() + } + + public func cancelPendingSendStarsReaction(id: EngineMessage.Id) { + let _ = cancelPendingSendStarsReactionInteractively(account: self.account, messageId: id).startStandalone() + } + + public func forceSendPendingSendStarsReaction(id: EngineMessage.Id) { + let _ = _internal_forceSendPendingSendStarsReaction(account: self.account, messageId: id).startStandalone() + } + + public func updateStarsReactionIsAnonymous(id: EngineMessage.Id, isAnonymous: Bool) -> Signal { + return _internal_updateStarsReactionIsAnonymous(account: self.account, messageId: id, isAnonymous: isAnonymous) } public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index d5175d928dc..5be3cc803b4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -11,6 +11,7 @@ public enum BotPaymentInvoiceSource { case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption) case stars(option: StarsTopUpOption) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) + case starsChatSubscription(hash: String) } public struct BotPaymentInvoiceFields: OptionSet { @@ -314,6 +315,8 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv return nil } return .inputInvoiceStars(purpose: .inputStorePaymentStarsGift(userId: inputUser, stars: count, currency: currency, amount: amount)) + case let .starsChatSubscription(hash): + return .inputInvoiceChatInviteSubscription(hash: hash) } } @@ -538,7 +541,7 @@ public enum SendBotPaymentFormError { } public enum SendBotPaymentResult { - case done(receiptMessageId: MessageId?) + case done(receiptMessageId: MessageId?, subscriptionPeerId: PeerId?) case externalVerificationRequired(url: String) } @@ -582,6 +585,17 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa case let .paymentResult(updates): account.stateManager.addUpdates(updates) var receiptMessageId: MessageId? + + switch source { + case .starsChatSubscription: + let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) } + if let first = chats.first { + return .done(receiptMessageId: nil, subscriptionPeerId: first.id) + } + default: + break + } + for apiMessage in updates.messages { if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { @@ -612,7 +626,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa receiptMessageId = id } } - case .giftCode, .stars, .starsGift: + case .giftCode, .stars, .starsGift, .starsChatSubscription: receiptMessageId = nil } } @@ -620,7 +634,7 @@ func _internal_sendBotPaymentForm(account: Account, formId: Int64, source: BotPa } } } - return .done(receiptMessageId: receiptMessageId) + return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil) case let .paymentVerificationNeeded(url): return .externalVerificationRequired(url: url) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 0bf8b5549c3..f93551a46ec 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -147,15 +147,18 @@ func _internal_starsGiftOptions(account: Account, peerId: EnginePeer.Id?) -> Sig struct InternalStarsStatus { let balance: Int64 + let subscriptionsMissingBalance: Int64? + let subscriptions: [StarsContext.State.Subscription] + let nextSubscriptionsOffset: String? let transactions: [StarsContext.State.Transaction] - let nextOffset: String? + let nextTransactionsOffset: String? } private enum RequestStarsStateError { case generic } -private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, mode: StarsTransactionsContext.Mode, offset: String?, limit: Int32) -> Signal { +private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id, mode: StarsTransactionsContext.Mode, subscriptionId: String?, offset: String?, limit: Int32) -> Signal { return account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } @@ -176,7 +179,10 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id default: break } - signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, peer: inputPeer, offset: offset, limit: limit)) + if let _ = subscriptionId { + flags = 1 << 3 + } + signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, subscriptionId: subscriptionId, peer: inputPeer, offset: offset, limit: limit)) } else { signal = account.network.request(Api.functions.payments.getStarsStatus(peer: inputPeer)) } @@ -187,17 +193,26 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> InternalStarsStatus in switch result { - case let .starsStatus(_, balance, history, nextOffset, chats, users): + case let .starsStatus(_, balance, _, _, subscriptionsMissingBalance, transactions, nextTransactionsOffset, chats, users): let peers = AccumulatedPeers(chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: peers) - + var parsedTransactions: [StarsContext.State.Transaction] = [] - for entry in history { - if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, peerId: peerId != account.peerId ? peerId : nil, transaction: transaction) { - parsedTransactions.append(parsedTransaction) + if let transactions { + for entry in transactions { + if let parsedTransaction = StarsContext.State.Transaction(apiTransaction: entry, peerId: peerId != account.peerId ? peerId : nil, transaction: transaction) { + parsedTransactions.append(parsedTransaction) + } } } - return InternalStarsStatus(balance: balance, transactions: parsedTransactions, nextOffset: nextOffset) + return InternalStarsStatus( + balance: balance, + subscriptionsMissingBalance: subscriptionsMissingBalance, + subscriptions: [], + nextSubscriptionsOffset: nil, + transactions: parsedTransactions, + nextTransactionsOffset: nextTransactionsOffset + ) } } |> castError(RequestStarsStateError.self) @@ -205,6 +220,58 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id } } +private enum RequestStarsSubscriptionsError { + case generic +} + +private func _internal_requestStarsSubscriptions(account: Account, peerId: EnginePeer.Id, offset: String, missingBalance: Bool) -> Signal { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> castError(RequestStarsSubscriptionsError.self) + |> mapToSignal { peer -> Signal in + guard let peer, let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + var flags: Int32 = 0 + if missingBalance { + flags |= (1 << 0) + } + return account.network.request(Api.functions.payments.getStarsSubscriptions(flags: flags, peer: inputPeer, offset: offset)) + |> retryRequest + |> castError(RequestStarsSubscriptionsError.self) + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> InternalStarsStatus in + switch result { + case let .starsStatus(_, balance, subscriptions, subscriptionsNextOffset, subscriptionsMissingBalance, _, _, chats, users): + let peers = AccumulatedPeers(chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: peers) + + var parsedSubscriptions: [StarsContext.State.Subscription] = [] + if let subscriptions { + for entry in subscriptions { + if let parsedSubscription = StarsContext.State.Subscription(apiSubscription: entry, transaction: transaction) { + if !missingBalance || parsedSubscription.flags.contains(.missingBalance) { + parsedSubscriptions.append(parsedSubscription) + } + } + } + } + return InternalStarsStatus( + balance: balance, + subscriptionsMissingBalance: subscriptionsMissingBalance, + subscriptions: parsedSubscriptions, + nextSubscriptionsOffset: subscriptionsNextOffset, + transactions: [], + nextTransactionsOffset: nil + ) + } + } + |> castError(RequestStarsSubscriptionsError.self) + } + } +} + private final class StarsContextImpl { private let account: Account fileprivate let peerId: EnginePeer.Id @@ -214,7 +281,6 @@ private final class StarsContextImpl { var state: Signal { return self._statePromise.get() } - private var nextOffset: String? private let disposable = MetaDisposable() private var updateDisposable: Disposable? @@ -235,7 +301,7 @@ private final class StarsContextImpl { guard let self, let state = self._state, let balance = balances[peerId] else { return } - self.updateState(StarsContext.State(flags: [], balance: balance, transactions: state.transactions, canLoadMore: nextOffset != nil, isLoading: false)) + self.updateState(StarsContext.State(flags: [], balance: balance, subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: state.transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: false)) self.load(force: true) }) } @@ -256,13 +322,12 @@ private final class StarsContextImpl { } self.previousLoadTimestamp = currentTimestamp - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, offset: nil, limit: 5) + self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, subscriptionId: nil, offset: nil, limit: 5) |> deliverOnMainQueue).start(next: { [weak self] status in guard let self else { return } - self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false)) - self.nextOffset = status.nextOffset + self.updateState(StarsContext.State(flags: [], balance: status.balance, subscriptions: status.subscriptions, canLoadMoreSubscriptions: status.nextSubscriptionsOffset != nil, transactions: status.transactions, canLoadMoreTransactions: status.nextTransactionsOffset != nil, isLoading: false)) }, error: { [weak self] _ in guard let self else { return @@ -273,39 +338,23 @@ private final class StarsContextImpl { })) } - func add(balance: Int64) { + func add(balance: Int64, addTransaction: Bool) { guard let state = self._state else { return } var transactions = state.transactions - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: []), at: 0) + if addTransaction { + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, media: [], subscriptionPeriod: nil), at: 0) + } - self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: state.balance + balance, transactions: transactions, canLoadMore: state.canLoadMore, isLoading: state.isLoading)) + self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(0, state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) } fileprivate func updateBalance(_ balance: Int64, transactions: [StarsContext.State.Transaction]?) { guard let state = self._state else { return } - self.updateState(StarsContext.State(flags: [], balance: balance, transactions: transactions ?? state.transactions, canLoadMore: state.canLoadMore, isLoading: state.isLoading)) - } - - func loadMore() { - assert(Queue.mainQueue().isCurrent()) - - guard let currentState = self._state, let nextOffset = self.nextOffset else { - return - } - - self._state?.isLoading = true - - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: .all, offset: nextOffset, limit: 10) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let self { - self.updateState(StarsContext.State(flags: [], balance: status.balance, transactions: currentState.transactions + status.transactions, canLoadMore: status.nextOffset != nil, isLoading: false)) - self.nextOffset = status.nextOffset - } - })) + self.updateState(StarsContext.State(flags: [], balance: balance, subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions ?? state.transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) } private func updateState(_ state: StarsContext.State) { @@ -317,7 +366,7 @@ private final class StarsContextImpl { private extension StarsContext.State.Transaction { init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { switch apiTransaction { - case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia): + case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod): let parsedPeer: StarsContext.State.Transaction.Peer var paidMessageId: MessageId? switch transactionPeer { @@ -360,9 +409,35 @@ private extension StarsContext.State.Transaction { if (apiFlags & (1 << 10)) != 0 { flags.insert(.isGift) } + if (apiFlags & (1 << 11)) != 0 { + flags.insert(.isReaction) + } let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] - self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media) + let _ = subscriptionPeriod + self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, media: media, subscriptionPeriod: subscriptionPeriod) + } + } +} + +private extension StarsContext.State.Subscription { + init?(apiSubscription: Api.StarsSubscription, transaction: Transaction) { + switch apiSubscription { + case let .starsSubscription(apiFlags, id, apiPeer, untilDate, pricing, inviteHash): + guard let peer = transaction.getPeer(apiPeer.peerId) else { + return nil + } + var flags: Flags = [] + if (apiFlags & (1 << 0)) != 0 { + flags.insert(.isCancelled) + } + if (apiFlags & (1 << 1)) != 0 { + flags.insert(.canRefulfill) + } + if (apiFlags & (1 << 2)) != 0 { + flags.insert(.missingBalance) + } + self.init(flags: flags, id: id, peer: EnginePeer(peer), untilDate: untilDate, pricing: StarsSubscriptionPricing(apiStarsSubscriptionPricing: pricing), inviteHash: inviteHash) } } } @@ -382,6 +457,7 @@ public final class StarsContext { public static let isPending = Flags(rawValue: 1 << 2) public static let isFailed = Flags(rawValue: 1 << 3) public static let isGift = Flags(rawValue: 1 << 4) + public static let isReaction = Flags(rawValue: 1 << 5) } public enum Peer: Equatable { @@ -406,6 +482,7 @@ public final class StarsContext { public let transactionUrl: String? public let paidMessageId: MessageId? public let media: [Media] + public let subscriptionPeriod: Int32? public init( flags: Flags, @@ -419,7 +496,8 @@ public final class StarsContext { transactionDate: Int32?, transactionUrl: String?, paidMessageId: MessageId?, - media: [Media] + media: [Media], + subscriptionPeriod: Int32? ) { self.flags = flags self.id = id @@ -433,6 +511,7 @@ public final class StarsContext { self.transactionUrl = transactionUrl self.paidMessageId = paidMessageId self.media = media + self.subscriptionPeriod = subscriptionPeriod } public static func == (lhs: Transaction, rhs: Transaction) -> Bool { @@ -472,6 +551,68 @@ public final class StarsContext { if !areMediaArraysEqual(lhs.media, rhs.media) { return false } + if lhs.subscriptionPeriod != rhs.subscriptionPeriod { + return false + } + return true + } + } + + public struct Subscription: Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isCancelled = Flags(rawValue: 1 << 0) + public static let canRefulfill = Flags(rawValue: 1 << 1) + public static let missingBalance = Flags(rawValue: 1 << 2) + } + + public let flags: Flags + public let id: String + public let peer: EnginePeer + public let untilDate: Int32 + public let pricing: StarsSubscriptionPricing + public let inviteHash: String? + + public init( + flags: Flags, + id: String, + peer: EnginePeer, + untilDate: Int32, + pricing: StarsSubscriptionPricing, + inviteHash: String? + ) { + self.flags = flags + self.id = id + self.peer = peer + self.untilDate = untilDate + self.pricing = pricing + self.inviteHash = inviteHash + } + + public static func == (lhs: Subscription, rhs: Subscription) -> Bool { + if lhs.flags != rhs.flags { + return false + } + if lhs.id != rhs.id { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.untilDate != rhs.untilDate { + return false + } + if lhs.pricing != rhs.pricing { + return false + } + if lhs.inviteHash != rhs.inviteHash { + return false + } return true } } @@ -488,15 +629,19 @@ public final class StarsContext { public var flags: Flags public var balance: Int64 + public var subscriptions: [Subscription] + public var canLoadMoreSubscriptions: Bool public var transactions: [Transaction] - public var canLoadMore: Bool + public var canLoadMoreTransactions: Bool public var isLoading: Bool - init(flags: Flags, balance: Int64, transactions: [Transaction], canLoadMore: Bool, isLoading: Bool) { + init(flags: Flags, balance: Int64, subscriptions: [Subscription], canLoadMoreSubscriptions: Bool, transactions: [Transaction], canLoadMoreTransactions: Bool, isLoading: Bool) { self.flags = flags self.balance = balance + self.subscriptions = subscriptions + self.canLoadMoreSubscriptions = canLoadMoreSubscriptions self.transactions = transactions - self.canLoadMore = canLoadMore + self.canLoadMoreTransactions = canLoadMoreTransactions self.isLoading = isLoading } @@ -510,7 +655,10 @@ public final class StarsContext { if lhs.transactions != rhs.transactions { return false } - if lhs.canLoadMore != rhs.canLoadMore { + if lhs.subscriptions != rhs.subscriptions { + return false + } + if lhs.canLoadMoreTransactions != rhs.canLoadMoreTransactions { return false } if lhs.isLoading != rhs.isLoading { @@ -542,7 +690,7 @@ public final class StarsContext { return peerId! } - var currentState: StarsContext.State? { + public var currentState: StarsContext.State? { var state: StarsContext.State? self.impl.syncWith { impl in state = impl._state @@ -550,9 +698,9 @@ public final class StarsContext { return state } - public func add(balance: Int64) { + public func add(balance: Int64, addTransaction: Bool = true) { self.impl.with { - $0.add(balance: balance) + $0.add(balance: balance, addTransaction: addTransaction) } } @@ -569,11 +717,6 @@ public final class StarsContext { } } - public func loadMore() { - self.impl.with { - $0.loadMore() - } - } init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { @@ -685,12 +828,12 @@ private final class StarsTransactionsContextImpl { updatedState.isLoading = true self.updateState(updatedState) - self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: self.mode, offset: nextOffset, limit: self.nextOffset == "" ? 25 : 50) + self.disposable.set((_internal_requestStarsState(account: self.account, peerId: self.peerId, mode: self.mode, subscriptionId: nil, offset: nextOffset, limit: self.nextOffset == "" ? 25 : 50) |> deliverOnMainQueue).start(next: { [weak self] status in guard let self else { return } - self.nextOffset = status.nextOffset + self.nextOffset = status.nextTransactionsOffset var updatedState = self._state updatedState.transactions = nextOffset.isEmpty ? status.transactions : updatedState.transactions + status.transactions @@ -769,6 +912,173 @@ public final class StarsTransactionsContext { } } +private final class StarsSubscriptionsContextImpl { + private let account: Account + private let missingBalance: Bool + + private var _state: StarsSubscriptionsContext.State + private let _statePromise = Promise() + var state: Signal { + return self._statePromise.get() + } + private var nextOffset: String? = "" + + private let disposable = MetaDisposable() + private var stateDisposable: Disposable? + private let updateDisposable = MetaDisposable() + + init(account: Account, starsContext: StarsContext?, missingBalance: Bool) { + assert(Queue.mainQueue().isCurrent()) + + self.account = account + self.missingBalance = missingBalance + + let currentSubscriptions = starsContext?.currentState?.subscriptions ?? [] + let canLoadMore = starsContext?.currentState?.canLoadMoreSubscriptions ?? true + + self._state = StarsSubscriptionsContext.State(balance: 0, subscriptions: currentSubscriptions, canLoadMore: canLoadMore, isLoading: false) + self._statePromise.set(.single(self._state)) + + self.loadMore() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + self.stateDisposable?.dispose() + self.updateDisposable.dispose() + } + + func loadMore() { + assert(Queue.mainQueue().isCurrent()) + + guard !self._state.isLoading, let nextOffset = self.nextOffset else { + return + } + + var updatedState = self._state + updatedState.isLoading = true + self.updateState(updatedState) + + self.disposable.set((_internal_requestStarsSubscriptions(account: self.account, peerId: self.account.peerId, offset: nextOffset, missingBalance: self.missingBalance) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self else { + return + } + self.nextOffset = status.nextSubscriptionsOffset + + var updatedState = self._state + updatedState.balance = status.subscriptionsMissingBalance ?? 0 + updatedState.subscriptions = nextOffset.isEmpty ? status.subscriptions : updatedState.subscriptions + status.subscriptions + updatedState.isLoading = false + updatedState.canLoadMore = self.nextOffset != nil + self.updateState(updatedState) + })) + } + + private func updateState(_ state: StarsSubscriptionsContext.State) { + self._state = state + self._statePromise.set(.single(state)) + } + + func updateSubscription(id: String, cancel: Bool) { + var updatedState = self._state + if let index = updatedState.subscriptions.firstIndex(where: { $0.id == id }) { + let subscription = updatedState.subscriptions[index] + var updatedFlags = subscription.flags + if cancel { + updatedFlags.insert(.isCancelled) + } else { + updatedFlags.remove(.isCancelled) + } + let updatedSubscription = StarsContext.State.Subscription(flags: updatedFlags, id: subscription.id, peer: subscription.peer, untilDate: subscription.untilDate, pricing: subscription.pricing, inviteHash: subscription.inviteHash) + updatedState.subscriptions[index] = updatedSubscription + } + self.updateState(updatedState) + self.updateDisposable.set(_internal_updateStarsSubscription(account: self.account, peerId: self.account.peerId, subscriptionId: id, cancel: cancel).startStrict()) + } + + private var previousLoadTimestamp: Double? + func load(force: Bool) { + assert(Queue.mainQueue().isCurrent()) + + let currentTimestamp = CFAbsoluteTimeGetCurrent() + if let previousLoadTimestamp = self.previousLoadTimestamp, currentTimestamp - previousLoadTimestamp < 60 && !force { + return + } + self.previousLoadTimestamp = currentTimestamp + + self.disposable.set((_internal_requestStarsSubscriptions(account: self.account, peerId: self.account.peerId, offset: "", missingBalance: false) + |> deliverOnMainQueue).start(next: { [weak self] status in + guard let self else { + return + } + self.nextOffset = status.nextSubscriptionsOffset + + var updatedState = self._state + updatedState.subscriptions = status.subscriptions + updatedState.isLoading = false + updatedState.canLoadMore = self.nextOffset != nil + self.updateState(updatedState) + })) + } +} + +public final class StarsSubscriptionsContext { + public struct State: Equatable { + public var balance: Int64 + public var subscriptions: [StarsContext.State.Subscription] + public var canLoadMore: Bool + public var isLoading: Bool + + init(balance: Int64, subscriptions: [StarsContext.State.Subscription], canLoadMore: Bool, isLoading: Bool) { + self.balance = balance + self.subscriptions = subscriptions + self.canLoadMore = canLoadMore + self.isLoading = isLoading + } + } + + fileprivate let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public func loadMore() { + self.impl.with { + $0.loadMore() + } + } + + init(account: Account, starsContext: StarsContext?, missingBalance: Bool) { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return StarsSubscriptionsContextImpl(account: account, starsContext: starsContext, missingBalance: missingBalance) + }) + } + + public func updateSubscription(id: String, cancel: Bool) { + self.impl.with { + $0.updateSubscription(id: id, cancel: cancel) + } + } + + public func load(force: Bool) { + self.impl.with { + $0.load(force: force) + } + } +} + + func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: BotPaymentInvoiceSource) -> Signal { return account.postbox.transaction { transaction -> Api.InputInvoice? in return _internal_parseInputInvoice(transaction: transaction, source: source) @@ -786,6 +1096,16 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot switch result { case let .paymentResult(updates): account.stateManager.addUpdates(updates) + + switch source { + case .starsChatSubscription: + let chats = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) } + if let first = chats.first { + return .done(receiptMessageId: nil, subscriptionPeerId: first.id) + } + default: + break + } var receiptMessageId: MessageId? for apiMessage in updates.messages { if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: false) { @@ -819,13 +1139,15 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot } case .giftCode, .stars, .starsGift: receiptMessageId = nil + case .starsChatSubscription: + receiptMessageId = nil } } } } } } - return .done(receiptMessageId: receiptMessageId) + return .done(receiptMessageId: receiptMessageId, subscriptionPeerId: nil) case let .paymentVerificationNeeded(url): return .externalVerificationRequired(url: url) } @@ -889,7 +1211,7 @@ func _internal_getStarsTransaction(accountPeerId: PeerId, postbox: Postbox, netw } |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> StarsContext.State.Transaction? in - guard let result, case let .starsStatus(_, _, transactions, _, chats, users) = result, let matchingTransaction = transactions.first else { + guard let result, case let .starsStatus(_, _, _, _, _, transactions, _, chats, users) = result, let matchingTransaction = transactions?.first else { return nil } let peers = AccumulatedPeers(chats: chats, users: users) @@ -900,3 +1222,91 @@ func _internal_getStarsTransaction(accountPeerId: PeerId, postbox: Postbox, netw } } } + +public struct StarsSubscriptionPricing: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case period + case amount + } + + public let period: Int32 + public let amount: Int64 + + public init(period: Int32, amount: Int64) { + self.period = period + self.amount = amount + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.period = try container.decode(Int32.self, forKey: .period) + self.amount = try container.decode(Int64.self, forKey: .amount) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.period, forKey: .period) + try container.encode(self.amount, forKey: .amount) + } + + public static let monthPeriod: Int32 = 2592000 + public static let testPeriod: Int32 = 300 +} + +extension StarsSubscriptionPricing { + init(apiStarsSubscriptionPricing: Api.StarsSubscriptionPricing) { + switch apiStarsSubscriptionPricing { + case let .starsSubscriptionPricing(period, amount): + self = .init(period: period, amount: amount) + } + } + + var apiStarsSubscriptionPricing: Api.StarsSubscriptionPricing { + return .starsSubscriptionPricing(period: self.period, amount: self.amount) + } +} + +public enum UpdateStarsSubsciptionError { + case generic +} + +func _internal_updateStarsSubscription(account: Account, peerId: EnginePeer.Id, subscriptionId: String, cancel: Bool) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(UpdateStarsSubsciptionError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + let flags: Int32 = (1 << 0) + return account.network.request(Api.functions.payments.changeStarsSubscription(flags: flags, peer: inputPeer, subscriptionId: subscriptionId, canceled: cancel ? .boolTrue : .boolFalse)) + |> mapError { _ -> UpdateStarsSubsciptionError in + return .generic + } + |> ignoreValues + } +} + +public enum FulfillStarsSubsciptionError { + case generic +} + +func _internal_fulfillStarsSubscription(account: Account, peerId: EnginePeer.Id, subscriptionId: String) -> Signal { + return account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(FulfillStarsSubsciptionError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .complete() + } + return account.network.request(Api.functions.payments.fulfillStarsSubscription(peer: inputPeer, subscriptionId: subscriptionId)) + |> mapError { _ -> FulfillStarsSubsciptionError in + return .generic + } + |> ignoreValues + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 062e457ea85..43b9079f7e0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -5,11 +5,11 @@ import Postbox public extension TelegramEngine { final class Payments { private let account: Account - + init(account: Account) { self.account = account } - + public func getBankCardInfo(cardNumber: String) -> Signal { return _internal_getBankCardInfo(account: self.account, cardNumber: cardNumber) } @@ -25,7 +25,7 @@ public extension TelegramEngine { public func validateBotPaymentForm(saveInfo: Bool, source: BotPaymentInvoiceSource, formInfo: BotPaymentRequestedInfo) -> Signal { return _internal_validateBotPaymentForm(account: self.account, saveInfo: saveInfo, source: source, formInfo: formInfo) } - + public func sendBotPaymentForm(source: BotPaymentInvoiceSource, formId: Int64, validatedInfoId: String?, shippingOptionId: String?, tipAmount: Int64?, credentials: BotPaymentCredentials) -> Signal { return _internal_sendBotPaymentForm(account: self.account, formId: formId, source: source, validatedInfoId: validatedInfoId, shippingOptionId: shippingOptionId, tipAmount: tipAmount, credentials: credentials) } @@ -33,7 +33,7 @@ public extension TelegramEngine { public func requestBotPaymentReceipt(messageId: MessageId) -> Signal { return _internal_requestBotPaymentReceipt(account: self.account, messageId: messageId) } - + public func clearBotPaymentInfo(info: BotPaymentInfo) -> Signal { return _internal_clearBotPaymentInfo(network: self.account.network, info: info) } @@ -86,8 +86,16 @@ public extension TelegramEngine { return StarsTransactionsContext(account: self.account, subject: subject, mode: mode) } + public func peerStarsSubscriptionsContext(starsContext: StarsContext?, missingBalance: Bool = false) -> StarsSubscriptionsContext { + return StarsSubscriptionsContext(account: self.account, starsContext: starsContext, missingBalance: missingBalance) + } + public func sendStarsPaymentForm(formId: Int64, source: BotPaymentInvoiceSource) -> Signal { return _internal_sendStarsPaymentForm(account: self.account, formId: formId, source: source) } + + public func fulfillStarsSubscription(peerId: EnginePeer.Id, subscriptionId: String) -> Signal { + return _internal_fulfillStarsSubscription(account: self.account, peerId: peerId, subscriptionId: subscriptionId) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index dfbda213134..27ad7034291 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -153,10 +153,10 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer if let peer = transaction.getPeer(peerId), let memberPeer = transaction.getPeer(memberId), let inputUser = apiInputUser(memberPeer) { if let channel = peer as? TelegramChannel, let inputChannel = apiInputChannel(channel) { let updatedParticipant: ChannelParticipant - if let currentParticipant = currentParticipant, case let .member(_, invitedAt, adminInfo, _, rank) = currentParticipant { - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank) + if let currentParticipant = currentParticipant, case let .member(_, invitedAt, adminInfo, _, rank, subscriptionUntilDate) = currentParticipant { + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } else { - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: nil, rank: nil) + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil) } return account.network.request(Api.functions.channels.inviteToChannel(channel: inputChannel, users: [inputUser])) |> `catch` { error -> Signal in @@ -208,7 +208,7 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer switch currentParticipant { case .creator: break - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo { wasBanned = true wasMember = !banInfo.rights.flags.contains(.banReadMessages) @@ -235,7 +235,7 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer if let presence = transaction.getPeerPresence(peerId: memberPeer.id) { presences[memberPeer.id] = presence } - if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant { + if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { if let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { peers[peer.id] = peer diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift index 3f7f1634219..cf9161b453b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddressNames.swift @@ -558,7 +558,7 @@ func _internal_adminedPublicChannels(account: Account, scope: AdminedPublicChann case let .chats(apiChats): chats = apiChats for chat in apiChats { - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _) = chat { subscriberCounts[chat.peerId] = participantsCount.flatMap(Int.init) } } @@ -630,7 +630,7 @@ func _internal_channelsForStories(account: Account) -> Signal<[Peer], NoError> { if let peer = transaction.getPeer(chat.peerId) { peers.append(peer) - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in var current = current as? CachedChannelData ?? CachedChannelData() var participantsSummary = current.participantsSummary diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index d113a071bc4..bc455b62093 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -50,6 +50,7 @@ public enum AdminLogEventAction { case changePhoto(prev: ([TelegramMediaImageRepresentation], [TelegramMediaImage.VideoRepresentation]), new: ([TelegramMediaImageRepresentation], [TelegramMediaImage.VideoRepresentation])) case toggleInvites(Bool) case toggleSignatures(Bool) + case toggleSignatureProfiles(Bool) case updatePinned(Message?) case editMessage(prev: Message, new: Message) case deleteMessage(Message) @@ -446,6 +447,8 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net action = .changeStatus(prev: PeerEmojiStatus(apiStatus: prevValue), new: PeerEmojiStatus(apiStatus: newValue)) case let .channelAdminLogEventActionChangeEmojiStickerSet(prevStickerset, newStickerset): action = .changeEmojiPack(prev: StickerPackReference(apiInputSet: prevStickerset), new: StickerPackReference(apiInputSet: newStickerset)) + case let .channelAdminLogEventActionToggleSignatureProfiles(newValue): + action = .toggleSignatureProfiles(boolFromApiValue(newValue)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) if let action = action { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift index ddeea363e8e..84e2522453f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift @@ -10,14 +10,14 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, return account.postbox.transaction { transaction -> Signal<(ChannelParticipant?, RenderedChannelParticipant?, Bool), NoError> in if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer), let _ = transaction.getPeer(account.peerId), let memberPeer = transaction.getPeer(memberId), let inputPeer = apiInputPeer(memberPeer) { let updatedParticipant: ChannelParticipant - if let currentParticipant = currentParticipant, case let .member(_, invitedAt, _, currentBanInfo, _) = currentParticipant { + if let currentParticipant = currentParticipant, case let .member(_, invitedAt, _, currentBanInfo, _, subscriptionUntilDate) = currentParticipant { let banInfo: ChannelParticipantBannedInfo? if let rights = rights, !rights.flags.isEmpty { banInfo = ChannelParticipantBannedInfo(rights: rights, restrictedBy: currentBanInfo?.restrictedBy ?? account.peerId, timestamp: currentBanInfo?.timestamp ?? Int32(Date().timeIntervalSince1970), isMember: currentBanInfo?.isMember ?? true) } else { banInfo = nil } - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: nil, banInfo: banInfo, rank: nil) + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: invitedAt, adminInfo: nil, banInfo: banInfo, rank: nil, subscriptionUntilDate: subscriptionUntilDate) } else { let banInfo: ChannelParticipantBannedInfo? if let rights = rights, !rights.flags.isEmpty { @@ -25,7 +25,7 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, } else { banInfo = nil } - updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: banInfo, rank: nil) + updatedParticipant = ChannelParticipant.member(id: memberId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: nil, banInfo: banInfo, rank: nil, subscriptionUntilDate: nil) } let apiRights: Api.ChatBannedRights @@ -48,7 +48,7 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, switch currentParticipant { case .creator: break - case let .member(_, _, adminInfo, banInfo, _): + case let .member(_, _, adminInfo, banInfo, _, _): if let _ = adminInfo { wasAdmin = true } @@ -131,7 +131,7 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, if let presence = transaction.getPeerPresence(peerId: memberPeer.id) { presences[memberPeer.id] = presence } - if case let .member(_, _, _, maybeBanInfo, _) = updatedParticipant, let banInfo = maybeBanInfo { + if case let .member(_, _, _, maybeBanInfo, _, _) = updatedParticipant, let banInfo = maybeBanInfo { if let peer = transaction.getPeer(banInfo.restrictedBy) { peers[peer.id] = peer } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift index b650134dae0..2394f8f5353 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift @@ -86,7 +86,7 @@ func _internal_updateChannelOwnership(account: Account, channelId: PeerId, membe let flags: TelegramChatAdminRightsFlags = TelegramChatAdminRightsFlags.peerSpecific(peer: .channel(channel)) let updatedParticipant = ChannelParticipant.creator(id: user.id, adminInfo: nil, rank: currentParticipant?.rank) - let updatedPreviousCreator = ChannelParticipant.member(id: accountUser.id, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: flags), promotedBy: accountUser.id, canBeEditedByAccountPeer: false), banInfo: nil, rank: currentCreator?.rank) + let updatedPreviousCreator = ChannelParticipant.member(id: accountUser.id, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: flags), promotedBy: accountUser.id, canBeEditedByAccountPeer: false), banInfo: nil, rank: currentCreator?.rank, subscriptionUntilDate: nil) let checkPassword = _internal_twoStepAuthData(account.network) |> mapError { error -> ChannelOwnershipTransferError in @@ -152,7 +152,7 @@ func _internal_updateChannelOwnership(account: Account, channelId: PeerId, membe switch currentParticipant { case .creator: wasAdmin = true - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let _ = adminInfo { wasAdmin = true } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift index ca5df1efee3..c2922254c12 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelRecommendation.swift @@ -117,7 +117,7 @@ func _internal_requestRecommendedChannels(account: Account, peerId: EnginePeer.I for chat in chats { if let peer = transaction.getPeer(chat.peerId) { peers.append(EnginePeer(peer)) - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _) = chat, let participantsCount = participantsCount { transaction.updatePeerCachedData(peerIds: Set([peer.id]), update: { _, current in var current = current as? CachedChannelData ?? CachedChannelData() var participantsSummary = current.participantsSummary diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift index 96c9eec56f2..31312c3b090 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift @@ -280,7 +280,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal Signal S var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = [] for chat in chats { - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _) = chat { if let participantsCount = participantsCount { memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift index 3a8881c801c..6886e3cd4d6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift @@ -31,7 +31,7 @@ func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel] var participantsCounts: [PeerId: Int32] = [:] for chat in chats { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _, _, _, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCountValue, _, _, _, _, _, _, _): if let participantsCountValue = participantsCountValue { participantsCounts[chat.peerId] = participantsCountValue } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift index c8c86a29cf8..5f4e7797d99 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InvitationLinks.swift @@ -44,7 +44,7 @@ func _internal_revokePersistentPeerExportedInvitation(account: Account, peerId: if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { let flags: Int32 = (1 << 2) if let _ = peer as? TelegramChannel { - return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil)) + return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil, subscriptionPricing: nil)) |> retryRequest |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> ExportedInvitation? in @@ -61,7 +61,7 @@ func _internal_revokePersistentPeerExportedInvitation(account: Account, peerId: } } } else if let _ = peer as? TelegramGroup { - return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil)) + return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: nil, usageLimit: nil, title: nil, subscriptionPricing: nil)) |> retryRequest |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> ExportedInvitation? in @@ -90,7 +90,7 @@ public enum CreatePeerExportedInvitationError { case generic } -func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?) -> Signal { +func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?, subscriptionPricing: StarsSubscriptionPricing?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var flags: Int32 = 0 @@ -106,7 +106,10 @@ func _internal_createPeerExportedInvitation(account: Account, peerId: PeerId, ti if let _ = title { flags |= (1 << 4) } - return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit, title: title)) + if let _ = subscriptionPricing { + flags |= (1 << 5) + } + return account.network.request(Api.functions.messages.exportChatInvite(flags: flags, peer: inputPeer, expireDate: expireDate, usageLimit: usageLimit, title: title, subscriptionPricing: subscriptionPricing?.apiStarsSubscriptionPricing)) |> mapError { _ in return CreatePeerExportedInvitationError.generic } |> map { result -> ExportedInvitation? in return ExportedInvitation(apiExportedInvite: result) @@ -634,6 +637,14 @@ public struct PeerInvitationImportersState: Equatable { public var about: String? public var approvedBy: PeerId? public var joinedViaFolderLink: Bool + + public init(peer: RenderedPeer, date: Int32, about: String? = nil, approvedBy: PeerId? = nil, joinedViaFolderLink: Bool) { + self.peer = peer + self.date = date + self.about = about + self.approvedBy = approvedBy + self.joinedViaFolderLink = joinedViaFolderLink + } } public var importers: [Importer] public var isLoadingMore: Bool @@ -817,7 +828,7 @@ private final class PeerInvitationImportersContextImpl { var link: String? var count: Int32 = 0 - if let invite = invite, case let .link(inviteLink, _, _, _, _, _, _, _, _, _, inviteCount, _) = invite { + if let invite = invite, case let .link(inviteLink, _, _, _, _, _, _, _, _, _, inviteCount, _, _) = invite { link = inviteLink if let inviteCount = inviteCount { count = inviteCount diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index c5971928426..94dacbdc8cc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift @@ -17,29 +17,35 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S |> take(1) |> castError(JoinChannelError.self) |> mapToSignal { peer -> Signal in - if let inputChannel = apiInputChannel(peer) { - let request: Signal - if let hash = hash { - request = account.network.request(Api.functions.messages.importChatInvite(hash: hash)) - } else { - request = account.network.request(Api.functions.channels.joinChannel(channel: inputChannel)) - } - return request - |> mapError { error -> JoinChannelError in - switch error.errorDescription { - case "CHANNELS_TOO_MUCH": - return .tooMuchJoined - case "USERS_TOO_MUCH": - return .tooMuchUsers - case "INVITE_REQUEST_SENT": - return .inviteRequestSent - default: - return .generic - } + + let request: Signal + if let hash = hash { + request = account.network.request(Api.functions.messages.importChatInvite(hash: hash)) + } else if let inputChannel = apiInputChannel(peer) { + request = account.network.request(Api.functions.channels.joinChannel(channel: inputChannel)) + } else { + request = .fail(.init()) + } + + return request + |> mapError { error -> JoinChannelError in + switch error.errorDescription { + case "CHANNELS_TOO_MUCH": + return .tooMuchJoined + case "USERS_TOO_MUCH": + return .tooMuchUsers + case "INVITE_REQUEST_SENT": + return .inviteRequestSent + default: + return .generic } - |> mapToSignal { updates -> Signal in - account.stateManager.addUpdates(updates) - + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + + let channels = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }.compactMap(apiInputChannel) + + if let inputChannel = channels.first { return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -64,7 +70,7 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S case let .channelParticipant(participant, _, _): updatedParticipant = ChannelParticipant(apiParticipant: participant) } - if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant { + if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { if let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { peers[peer.id] = peer @@ -76,14 +82,16 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S } |> castError(JoinChannelError.self) } + } else { + return .fail(.generic) } - |> afterCompleted { - if hash == nil { - let _ = _internal_requestRecommendedChannels(account: account, peerId: peerId, forceUpdate: true).startStandalone() - } + + + } + |> afterCompleted { + if hash == nil { + let _ = _internal_requestRecommendedChannels(account: account, peerId: peerId, forceUpdate: true).startStandalone() } - } else { - return .fail(.generic) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index be5f03b5a7e..a73977553b3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -39,6 +39,7 @@ public enum ExternalJoiningChatState { public let isVerified: Bool public let isScam: Bool public let isFake: Bool + public let canRefulfillSubscription: Bool } public let flags: Flags @@ -48,6 +49,8 @@ public enum ExternalJoiningChatState { public let participantsCount: Int32 public let participants: [EnginePeer]? public let nameColor: PeerNameColor? + public let subscriptionPricing: StarsSubscriptionPricing? + public let subscriptionFormId: Int64? } case invite(Invite) @@ -106,10 +109,10 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal mapToSignal { result -> Signal in if let result = result { switch result { - case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants, nameColor): + case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants, nameColor, subscriptionPricing, subscriptionFormId): let photo = telegramMediaImageFromApiPhoto(invitePhoto).flatMap({ smallestImageRepresentation($0.representations) }) - let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0, isVerified: (flags & (1 << 7)) != 0, isScam: (flags & (1 << 8)) != 0, isFake: (flags & (1 << 9)) != 0) - return .single(.invite(ExternalJoiningChatState.Invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({ EnginePeer(TelegramUser(user: $0)) }), nameColor: PeerNameColor(rawValue: nameColor)))) + let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0, isVerified: (flags & (1 << 7)) != 0, isScam: (flags & (1 << 8)) != 0, isFake: (flags & (1 << 9)) != 0, canRefulfillSubscription: (flags & (1 << 11)) != 0) + return .single(.invite(ExternalJoiningChatState.Invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({ EnginePeer(TelegramUser(user: $0)) }), nameColor: PeerNameColor(rawValue: nameColor), subscriptionPricing: subscriptionPricing.flatMap { StarsSubscriptionPricing(apiStarsSubscriptionPricing: $0) }, subscriptionFormId: subscriptionFormId))) case let .chatInviteAlready(chat): if let peer = parseTelegramGroupOrChannel(chat: chat) { return account.postbox.transaction({ (transaction) -> ExternalJoiningChatState in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 9fd972a6756..1f67bde1b43 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -510,6 +510,10 @@ public extension EnginePeer { var isPremium: Bool { return self._asPeer().isPremium } + + var isSubscription: Bool { + return self._asPeer().isSubscription + } var isService: Bool { if case let .user(peer) = self { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift index 5df09814567..2a3dca7538f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift @@ -158,14 +158,14 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI if let peer = transaction.getPeer(peerId), let adminPeer = transaction.getPeer(adminId), let inputUser = apiInputUser(adminPeer) { if let channel = peer as? TelegramChannel, let inputChannel = apiInputChannel(channel) { let updatedParticipant: ChannelParticipant - if let currentParticipant = currentParticipant, case let .member(_, invitedAt, currentAdminInfo, _, _) = currentParticipant { + if let currentParticipant = currentParticipant, case let .member(_, invitedAt, currentAdminInfo, _, _, subscriptionUntilDate) = currentParticipant { let adminInfo: ChannelParticipantAdminInfo? if let rights = rights { adminInfo = ChannelParticipantAdminInfo(rights: rights, promotedBy: currentAdminInfo?.promotedBy ?? account.peerId, canBeEditedByAccountPeer: true) } else { adminInfo = nil } - updatedParticipant = .member(id: adminId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank) + updatedParticipant = .member(id: adminId, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: nil, rank: rank, subscriptionUntilDate: subscriptionUntilDate) } else if let currentParticipant = currentParticipant, case .creator = currentParticipant { let adminInfo: ChannelParticipantAdminInfo? if let rights = rights { @@ -181,7 +181,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI } else { adminInfo = nil } - updatedParticipant = .member(id: adminId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: adminInfo, banInfo: nil, rank: rank) + updatedParticipant = .member(id: adminId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: adminInfo, banInfo: nil, rank: rank, subscriptionUntilDate: nil) } return account.network.request(Api.functions.channels.editAdmin(channel: inputChannel, userId: inputUser, adminRights: rights?.apiAdminRights ?? .chatAdminRights(flags: 0), rank: rank ?? "")) |> map { [$0] } @@ -225,7 +225,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI switch currentParticipant { case .creator: wasAdmin = true - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let _ = adminInfo { wasAdmin = true } @@ -248,7 +248,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI if let presence = transaction.getPeerPresence(peerId: adminPeer.id) { presences[adminPeer.id] = presence } - if case let .member(_, _, maybeAdminInfo, _, _) = updatedParticipant, let adminInfo = maybeAdminInfo { + if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant, let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { peers[peer.id] = peer } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index d73383b721d..f6099c2e550 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -42,7 +42,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { switch chat { - case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _): + case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _): if let participantsCount = participantsCount { subscribers[groupOrChannel.id] = participantsCount } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 948d724634a..18577aff3e0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -458,8 +458,8 @@ public extension TelegramEngine { } } - public func toggleShouldChannelMessagesSignatures(peerId: PeerId, enabled: Bool) -> Signal { - return _internal_toggleShouldChannelMessagesSignatures(account: self.account, peerId: peerId, enabled: enabled) + public func toggleShouldChannelMessagesSignatures(peerId: PeerId, signaturesEnabled: Bool, profilesEnabled: Bool) -> Signal { + return _internal_toggleShouldChannelMessagesSignatures(account: self.account, peerId: peerId, signaturesEnabled: signaturesEnabled, profilesEnabled: profilesEnabled) } public func toggleMessageCopyProtection(peerId: PeerId, enabled: Bool) -> Signal { @@ -705,8 +705,8 @@ public extension TelegramEngine { return _internal_checkPeerChatServiceActions(postbox: self.account.postbox, peerId: peerId) } - public func createPeerExportedInvitation(peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?) -> Signal { - return _internal_createPeerExportedInvitation(account: self.account, peerId: peerId, title: title, expireDate: expireDate, usageLimit: usageLimit, requestNeeded: requestNeeded) + public func createPeerExportedInvitation(peerId: PeerId, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?, subscriptionPricing: StarsSubscriptionPricing?) -> Signal { + return _internal_createPeerExportedInvitation(account: self.account, peerId: peerId, title: title, expireDate: expireDate, usageLimit: usageLimit, requestNeeded: requestNeeded, subscriptionPricing: subscriptionPricing) } public func editPeerExportedInvitation(peerId: PeerId, link: String, title: String?, expireDate: Int32?, usageLimit: Int32?, requestNeeded: Bool?) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ToggleChannelSignatures.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ToggleChannelSignatures.swift index e6ed0ae26d2..bec0218e313 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ToggleChannelSignatures.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ToggleChannelSignatures.swift @@ -5,10 +5,17 @@ import TelegramApi import MtProtoKit -func _internal_toggleShouldChannelMessagesSignatures(account: Account, peerId: PeerId, enabled: Bool) -> Signal { +func _internal_toggleShouldChannelMessagesSignatures(account: Account, peerId: PeerId, signaturesEnabled: Bool, profilesEnabled: Bool) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) as? TelegramChannel, let inputChannel = apiInputChannel(peer) { - return account.network.request(Api.functions.channels.toggleSignatures(channel: inputChannel, enabled: enabled ? .boolTrue : .boolFalse)) |> retryRequest |> map { updates -> Void in + var flags: Int32 = 0 + if signaturesEnabled { + flags |= 1 << 0 + } + if profilesEnabled { + flags |= 1 << 1 + } + return account.network.request(Api.functions.channels.toggleSignatures(flags: flags, channel: inputChannel)) |> retryRequest |> map { updates -> Void in account.stateManager.addUpdates(updates) } } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift index dd80946fe63..1ae8ef638f5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateBotInfo.swift @@ -100,7 +100,7 @@ func _internal_updateBotDescription(account: Account, peerId: PeerId, descriptio if let botInfo = current.botInfo { var updatedBotInfo = botInfo if botInfo.description == editableBotInfo.description { - updatedBotInfo = BotInfo(description: description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: botInfo.menuButton) + updatedBotInfo = BotInfo(description: description, photo: botInfo.photo, video: botInfo.video, commands: botInfo.commands, menuButton: botInfo.menuButton, privacyPolicyUrl: botInfo.privacyPolicyUrl) } return current.withUpdatedEditableBotInfo(editableBotInfo.withUpdatedDescription(description)).withUpdatedBotInfo(updatedBotInfo) } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 8f1c5fa8e2f..6f17d552baf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -371,7 +371,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee var subscriberCount: Int32? for chat in chats { if chat.peerId == channelPeerId { - if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _) = chat { + if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _, _, _, _, _, _, _) = chat { subscriberCount = participantsCount } } @@ -439,7 +439,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee var botInfos: [CachedPeerBotInfo] = [] for botInfo in chatFullBotInfo ?? [] { switch botInfo { - case let .botInfo(_, userId, _, _, _, _, _): + case let .botInfo(_, userId, _, _, _, _, _, _): if let userId = userId { let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let parsedBotInfo = BotInfo(apiBotInfo: botInfo) @@ -450,7 +450,6 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let participants = CachedGroupParticipants(apiParticipants: chatFullParticipants) let autoremoveTimeout: CachedPeerAutoremoveTimeout = .known(CachedPeerAutoremoveTimeout.Value(chatTtlPeriod)) - var invitedBy: PeerId? if let participants = participants { @@ -513,7 +512,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } else { mappedAllowedReactions = .empty } - let mappedReactionSettings = PeerReactionSettings(allowedReactions: mappedAllowedReactions, maxReactionCount: reactionsLimit) + + let mappedReactionSettings = PeerReactionSettings(allowedReactions: mappedAllowedReactions, maxReactionCount: reactionsLimit, starsAllowed: nil) return previous.withUpdatedParticipants(participants) .withUpdatedExportedInvitation(exportedInvitation) @@ -560,10 +560,10 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch result { case let .chatFull(fullChat, chats, users): switch fullChat { - case let .channelFull(_, _, _, _, _, _, _, _, _, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): - transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: notifySettings)]) - case .chatFull: - break + case let .channelFull(_, _, _, _, _, _, _, _, _, _, _, _, _, notifySettings, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + transaction.updateCurrentPeerNotificationSettings([peerId: TelegramPeerNotificationSettings(apiSettings: notifySettings)]) + case .chatFull: + break } switch fullChat { @@ -629,7 +629,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee var botInfos: [CachedPeerBotInfo] = [] for botInfo in apiBotInfos { switch botInfo { - case let .botInfo(_, userId, _, _, _, _, _): + case let .botInfo(_, userId, _, _, _, _, _, _): if let userId = userId { let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let parsedBotInfo = BotInfo(apiBotInfo: botInfo) @@ -695,7 +695,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch participantResult { case let .channelParticipant(participant, _, _): switch participant { - case let .channelParticipantSelf(flags, _, inviterId, invitedDate): + case let .channelParticipantSelf(flags, _, inviterId, invitedDate, _): invitedBy = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId)) if (flags & (1 << 0)) != 0 { invitedOn = invitedDate @@ -758,7 +758,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } else { mappedAllowedReactions = .empty } - let mappedReactionSettings = PeerReactionSettings(allowedReactions: mappedAllowedReactions, maxReactionCount: reactionsLimit) + let starsAllowed: Bool = (flags2 & (1 << 16)) != 0 + let mappedReactionSettings = PeerReactionSettings(allowedReactions: mappedAllowedReactions, maxReactionCount: reactionsLimit, starsAllowed: starsAllowed) let membersHidden = (flags2 & (1 << 2)) != 0 let forumViewAsMessages = (flags2 & (1 << 6)) != 0 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift index d84d5e812a2..d8ada707cd7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/UpdatedAccountPrivacySettings.swift @@ -180,7 +180,7 @@ func _internal_requestAccountPrivacySettings(account: Account) -> Signal Bool { + func isSensitiveContent(platform: String) -> Bool { + if let rule = self.restrictedContentAttribute?.rules.first(where: { $0.reason == "sensitive" }) { + if rule.platform == "all" || rule.platform == platform { + return true + } + } + if let peer = self.peers[self.id.peerId], peer.hasSensitiveContent(platform: platform) { + return true + } return false } } @@ -530,6 +541,15 @@ public extension Message { var paidContent: TelegramMediaPaidContent? { return self.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent } + + var authorSignatureAttribute: AuthorSignatureMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? AuthorSignatureMessageAttribute { + return attribute + } + } + return nil + } } public extension Message { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index e068920e0d3..346ea6c676d 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -57,6 +57,9 @@ public extension Peer { if let restrictionInfo = restrictionInfo { for rule in restrictionInfo.rules { + if rule.reason == "sensitive" { + continue + } if rule.platform == "all" || rule.platform == platform || contentSettings.addContentRestrictionReasons.contains(rule.platform) { if !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { // MARK: Nicegram @@ -223,6 +226,15 @@ public extension Peer { } } + var isSubscription: Bool { + switch self { + case let channel as TelegramChannel: + return channel.subscriptionUntilDate != nil + default: + return false + } + } + var isCloseFriend: Bool { switch self { case let user as TelegramUser: @@ -243,6 +255,25 @@ public extension Peer { } } + func hasSensitiveContent(platform: String) -> Bool { + var restrictionInfo: PeerAccessRestrictionInfo? + switch self { + case let user as TelegramUser: + restrictionInfo = user.restrictionInfo + case let channel as TelegramChannel: + restrictionInfo = channel.restrictionInfo + default: + break + } + + if let restrictionInfo, let rule = restrictionInfo.rules.first(where: { $0.reason == "sensitive" }) { + if rule.platform == "all" || rule.platform == platform { + return true + } + } + return false + } + var isForum: Bool { if let channel = self as? TelegramChannel { return channel.flags.contains(.isForum) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 4749936845e..d80705ed74f 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -534,7 +534,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -550,7 +550,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -572,7 +572,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -588,7 +588,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -607,7 +607,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -623,7 +623,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index 24cd2164c4e..c3f7ff89ede 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -747,7 +747,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -763,7 +763,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -783,7 +783,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -799,7 +799,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) @@ -818,7 +818,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ), @@ -834,7 +834,7 @@ public func makeDefaultDarkTintedPresentationTheme(extendingThemeReference: Pres reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1), reactionActiveMediaPlaceholder: UIColor(rgb: 0x000000, alpha: 0.1) ) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 6e8c85faa96..69b9a6813a8 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -601,7 +601,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -617,7 +617,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -656,7 +656,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -672,7 +672,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -713,7 +713,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -729,7 +729,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -765,7 +765,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -781,7 +781,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -823,7 +823,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -839,7 +839,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) @@ -880,7 +880,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ), @@ -896,7 +896,7 @@ public func makeDefaultDayPresentationTheme(extendingThemeReference: Presentatio reactionStarsInactiveBackground: UIColor(rgb: 0xFEF1D4, alpha: 1.0), reactionStarsInactiveForeground: UIColor(rgb: 0xD3720A), reactionStarsActiveBackground: UIColor(rgb: 0xD3720A, alpha: 1.0), - reactionStarsActiveForeground: .clear, + reactionStarsActiveForeground: .white, reactionInactiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2), reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2) ) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 1816f40ef87..4bdc13490f1 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -751,11 +751,10 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, if message.author?.id == accountPeerId { attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } else { - //TODO:localize var authorName = compactAuthorName var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { - authorName = "Unknown user" + authorName = strings.Notification_StarsGift_UnknownUser peerIds = [] } var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) @@ -1010,13 +1009,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } case let .paymentRefunded(peerId, currency, totalAmount, _, _): - //TODO:localize - let patternString: String - if peerId == message.id.peerId { - patternString = "You received a refund of {amount}" - } else { - patternString = "You received a refund of {amount} from {name}" - } + let patternString = strings.Notification_Refund let mutableString = NSMutableAttributedString() mutableString.append(NSAttributedString(string: patternString, font: titleFont, textColor: primaryTextColor)) diff --git a/submodules/TelegramStringFormatting/Sources/WeatherFormat.swift b/submodules/TelegramStringFormatting/Sources/WeatherFormat.swift index 9adc721d4f9..80e004b1500 100644 --- a/submodules/TelegramStringFormatting/Sources/WeatherFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/WeatherFormat.swift @@ -33,12 +33,15 @@ private func currentTemperatureUnit() -> TemperatureUnit { return temperatureUnit } -public func stringForTemperature(_ value: Double) -> String { +private var formatter: MeasurementFormatter = { let formatter = MeasurementFormatter() formatter.locale = Locale.current formatter.unitStyle = .short formatter.numberFormatter.maximumFractionDigits = 0 - formatter.unitOptions = .temperatureWithoutUnit + return formatter +}() + +public func stringForTemperature(_ value: Double) -> String { let valueString = formatter.string(from: Measurement(value: value, unit: UnitTemperature.celsius)).trimmingCharacters(in: CharacterSet(charactersIn: "0123456789-,.").inverted) return valueString + currentTemperatureUnit().suffix } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 248feb61b63..578a33efaf3 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -18,13 +18,13 @@ NGDEPS = [ "//Nicegram/NGModels:NGModels", "//Nicegram/NGWrap:NGWrap", "@FirebaseSDK//:FirebaseCrashlytics", + "@swiftpkg_nicegram_assistant_ios//:FeatAssistant", "@swiftpkg_nicegram_assistant_ios//:FeatAuth", "@swiftpkg_nicegram_assistant_ios//:FeatAvatarGeneratorUI", "@swiftpkg_nicegram_assistant_ios//:FeatChatBanner", "@swiftpkg_nicegram_assistant_ios//:FeatOnboarding", "@swiftpkg_nicegram_assistant_ios//:FeatPremium", "@swiftpkg_nicegram_assistant_ios//:FeatTgChatButton", - "@swiftpkg_nicegram_assistant_ios//:NGAssistantUI", "@swiftpkg_nicegram_assistant_ios//:NGEntryPoint", "@swiftpkg_nicegram_assistant_ios//:_NGRemoteConfig", ] diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index 439be5c6bff..8259e279657 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -531,7 +531,7 @@ private final class AdminUserActionsSheetComponent: Component { allowedParticipantRights = [] allowedMediaRights = [] break loop - case let .member(_, _, adminInfo, banInfo, _): + case let .member(_, _, adminInfo, banInfo, _, _): if adminInfo != nil { (allowedParticipantRights, allowedMediaRights) = rightsFromBannedRights([]) break loop @@ -625,7 +625,7 @@ private final class AdminUserActionsSheetComponent: Component { switch peer.participant { case .creator: canBanEveryone = false - case let .member(_, _, adminInfo, banInfo, _): + case let .member(_, _, adminInfo, banInfo, _, _): let _ = banInfo if let adminInfo { if channel.flags.contains(.isCreator) { diff --git a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift index f55bdaa96c7..25856d10133 100644 --- a/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsInfoScreen/Sources/AdsInfoScreen.swift @@ -488,7 +488,8 @@ public final class AdsInfoScreen: ViewControllerComponentContainer { private let context: AccountContext public init( - context: AccountContext + context: AccountContext, + forceDark: Bool = false ) { self.context = context @@ -503,7 +504,7 @@ public final class AdsInfoScreen: ViewControllerComponentContainer { ), navigationBarAppearance: .none, statusBarStyle: .ignore, - theme: .default + theme: forceDark ? .dark : .default ) self.navigationPresentation = .modal diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 49dccb428ee..86ee3fe125d 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -573,6 +573,7 @@ public final class AdsReportScreen: ViewControllerComponentContainer { opaqueId: Data, title: String, options: [ReportAdMessageResult.Option], + forceDark: Bool = false, completed: @escaping () -> Void ) { self.context = context @@ -593,7 +594,7 @@ public final class AdsReportScreen: ViewControllerComponentContainer { ), navigationBarAppearance: .none, statusBarStyle: .ignore, - theme: .default + theme: forceDark ? .dark : .default ) self.navigationPresentation = .flatModal diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index 5c3a6f05467..bb26607690c 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -76,6 +76,8 @@ public final class AnimatedTextComponent: Component { let delayNorm: CGFloat = 0.002 + var firstDelayWidth: CGFloat? + var validKeys: [CharacterKey] = [] for item in component.items { var itemText: [String] = [] @@ -138,20 +140,32 @@ public final class AnimatedTextComponent: Component { if characterTransition.animation.isImmediate { characterComponentView.frame = characterFrame } else { + var delayWidth: Double = 0.0 + if let firstDelayWidth { + delayWidth = size.width - firstDelayWidth + } else { + firstDelayWidth = size.width + } + characterComponentView.bounds = CGRect(origin: CGPoint(), size: characterFrame.size) let deltaPosition = CGPoint(x: characterFrame.midX - characterComponentView.frame.midX, y: characterFrame.midY - characterComponentView.frame.midY) characterComponentView.center = characterFrame.center - characterComponentView.layer.animatePosition(from: CGPoint(x: -deltaPosition.x, y: -deltaPosition.y), to: CGPoint(), duration: 0.4, delay: delayNorm * size.width, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + characterComponentView.layer.animatePosition(from: CGPoint(x: -deltaPosition.x, y: -deltaPosition.y), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } characterTransition.setFrame(view: characterComponentView, frame: characterFrame) - if animateIn, !transition.animation.isImmediate { - characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * size.width, timingFunction: kCAMediaTimingFunctionSpring) - //characterComponentView.layer.animateSpring(from: (characterSize.height * 0.5) as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", duration: 0.5, additive: true) - characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * size.width, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * size.width) + var delayWidth: Double = 0.0 + if let firstDelayWidth { + delayWidth = size.width - firstDelayWidth + } else { + firstDelayWidth = size.width + } + + characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) + characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth) } } @@ -160,6 +174,11 @@ public final class AnimatedTextComponent: Component { } } + let outScaleTransition: ComponentTransition = .spring(duration: 0.4) + let outAlphaTransition: ComponentTransition = .easeInOut(duration: 0.18) + + var outFirstDelayWidth: CGFloat? + var removedKeys: [CharacterKey] = [] for (key, characterView) in self.characters { if !validKeys.contains(key) { @@ -167,9 +186,16 @@ public final class AnimatedTextComponent: Component { if let characterComponentView = characterView.view { if !transition.animation.isImmediate { - characterComponentView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.4, delay: delayNorm * characterComponentView.frame.minX, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - characterComponentView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -characterComponentView.bounds.height * 0.4), duration: 0.4, delay: delayNorm * characterComponentView.frame.minX, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - characterComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: delayNorm * characterComponentView.frame.minX, removeOnCompletion: false, completion: { [weak characterComponentView] _ in + var delayWidth: Double = 0.0 + if let outFirstDelayWidth { + delayWidth = characterComponentView.frame.minX - outFirstDelayWidth + } else { + outFirstDelayWidth = characterComponentView.frame.minX + } + + outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth) + outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: characterComponentView.center.y - characterComponentView.bounds.height * 0.4), delay: delayNorm * delayWidth) + outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in characterComponentView?.removeFromSuperview() }) } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 1917457a40d..865e0b55acd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -33,6 +33,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { public var storyData: (hasUnseen: Bool, hasUnseenCloseFriends: Bool)? public let statusView: ComponentView + private var starView: StarView? private var cachedDataDisposable = MetaDisposable() private var hierarchyTrackingLayer: HierarchyTrackingLayer? @@ -120,6 +121,25 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { self.context = context self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: displayDimensions, storeUnrounded: storeUnrounded) + if let peer, peer.isSubscription { + let starView: StarView + if let current = self.starView { + starView = current + } else { + starView = StarView() + self.starView = starView + self.containerNode.view.addSubview(starView) + } + starView.outlineColor = theme.rootController.navigationBar.opaqueBackgroundColor + + let starSize = CGSize(width: 15.0, height: 15.0) + let starFrame = CGRect(origin: CGPoint(x: self.containerNode.bounds.width - starSize.width + 1.0, y: self.containerNode.bounds.height - starSize.height + 1.0), size: starSize) + starView.frame = starFrame + } else if let starView = self.starView { + self.starView = nil + starView.removeFromSuperview() + } + if let peer = peer, peer.isPremium { self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id) |> deliverOnMainQueue).start(next: { [weak self] peerView in @@ -283,3 +303,33 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { } } } + +private class StarView: UIView { + let outline = SimpleLayer() + let foreground = SimpleLayer() + + var outlineColor: UIColor = .white { + didSet { + self.outline.layerTintColor = self.outlineColor.cgColor + } + } + + override init(frame: CGRect) { + self.outline.contents = UIImage(bundleImageName: "Premium/Stars/StarMediumOutline")?.cgImage + self.foreground.contents = UIImage(bundleImageName: "Premium/Stars/StarMedium")?.cgImage + + super.init(frame: frame) + + self.layer.addSublayer(self.outline) + self.layer.addSublayer(self.foreground) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + override func layoutSubviews() { + self.outline.frame = self.bounds + self.foreground.frame = self.bounds + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 0206dacbb74..8edf85c6886 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -664,6 +664,8 @@ public final class ChatInlineSearchResultsListComponent: Component { }, openStories: { _, _ in }, + openStarsTopup: { _ in + }, dismissNotice: { _ in }, editPeer: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 430efe041f6..ab309b8b745 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1275,9 +1275,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let reactions: ReactionsMessageAttribute if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) { - reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? if !reactions.reactions.isEmpty { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 4ee70dcddd5..6ef71e157fd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -318,6 +318,12 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { flags.remove(.preferMediaInline) mediaAndFlags = (mediaAndFlagsValue.0, flags) } + if let adAttribute = message.adAttribute, adAttribute.hasContentMedia { + var flags = mediaAndFlagsValue.1 + flags.remove(.preferMediaInline) + flags.insert(.preferMediaBeforeText) + mediaAndFlags = (mediaAndFlagsValue.0, flags) + } } var contentMediaAspectFilled = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 530eec60ee9..a604a7e798e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -873,6 +873,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI for contentNode in self.contentNodes { contentNode.updateIsExtractedToContextPreview(isExtractedToContextPreview) } + + if !isExtractedToContextPreview { + if let item = self.item { + item.controllerInteraction.forceUpdateWarpContents() + } + } } self.mainContextSourceNode.updateAbsoluteRect = { [weak self] rect, size in @@ -1506,6 +1512,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } var effectiveAuthor: Peer? + var overrideEffectiveAuthor = false var ignoreForward = false var displayAuthorInfo: Bool var ignoreNameHiding = false @@ -1574,6 +1581,27 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI displayAuthorInfo = false } } + + if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, firstMessage.author?.id != channel.id { + if info.flags.contains(.messagesShouldHaveProfiles) { + var allowAuthor = incoming + overrideEffectiveAuthor = true + + if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview { + allowAuthor = true + ignoreNameHiding = true + } + + if let subject = item.associatedData.subject, case let .customChatContents(contents) = subject, case .hashTagSearch = contents.kind { + ignoreNameHiding = true + } + + displayAuthorInfo = !mergedTop.merged && allowAuthor && peerId.isGroupOrChannel && effectiveAuthor != nil + if let forwardInfo = firstMessage.forwardInfo, forwardInfo.psaType != nil { + displayAuthorInfo = false + } + } + } if !peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { if peerId.isGroupOrChannel && effectiveAuthor != nil { @@ -1591,6 +1619,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI hasAvatar = incoming } else if case .customChatContents = item.chatLocation { hasAvatar = false + } else if overrideEffectiveAuthor { + hasAvatar = true } } } else if incoming { @@ -2123,9 +2153,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let bubbleReactions: ReactionsMessageAttribute if needReactions { - bubbleReactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + bubbleReactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - bubbleReactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + bubbleReactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { bottomNodeMergeStatus = .Both @@ -2173,7 +2203,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if initialDisplayHeader && displayAuthorInfo { - if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { + if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil, !overrideEffectiveAuthor { authorNameString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) let peer = (peer as Peer) @@ -6147,7 +6177,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } override public func makeProgress() -> Promise? { - return self.unlockButtonNode?.makeProgress() + if let unlockButtonNode = self.unlockButtonNode { + return unlockButtonNode.makeProgress() + } else { + for contentNode in self.contentNodes { + if let webpageContentNode = contentNode as? ChatMessageWebpageBubbleContentNode { + return webpageContentNode.contentNode.makeProgress() + } + } + } + return nil } override public func targetReactionView(value: MessageReaction.Reaction) -> UIView? { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index ce5c3d34b38..7d076a23c51 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -727,7 +727,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { var animationFileId: Int64? switch reaction.value { - case .builtin: + case .builtin, .stars: if let availableReactions = arguments.availableReactions { for availableReaction in availableReactions.reactions { if availableReaction.value == reaction.value { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index ef3ac7d6ff3..251d8806121 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -126,16 +126,22 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess var authorTitle: String? if let author = message.author as? TelegramUser { if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - authorTitle = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) { + } else { + authorTitle = EnginePeer(author).displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } } else if let forwardInfo = message.forwardInfo, forwardInfo.sourceMessageId?.peerId.namespace == Namespaces.Peer.CloudChannel { authorTitle = forwardInfo.authorSignature } } else { if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { - for attribute in message.attributes { - if let attribute = attribute as? AuthorSignatureMessageAttribute { - authorTitle = attribute.signature - break + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id, info.flags.contains(.messagesShouldHaveProfiles) { + } else { + for attribute in message.attributes { + if let attribute = attribute as? AuthorSignatureMessageAttribute { + authorTitle = attribute.signature + break + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index f9c0c49728d..efc4d58fcdb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -595,9 +595,9 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco let reactions: ReactionsMessageAttribute if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) { - reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 4571bc10740..530fa155fab 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -217,7 +217,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { private let context: AccountContext private let blurredImageNode: TransformImageNode - private let dustNode: MediaDustNode + fileprivate let dustNode: MediaDustNode fileprivate let buttonNode: HighlightTrackingButtonNode private let highlightedBackgroundNode: ASDisplayNode private let iconNode: ASImageNode @@ -306,6 +306,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { func reveal(animated: Bool = false) { self.isRevealed = true if animated { + self.dustNode.revealOnTap = true self.dustNode.tap(at: CGPoint(x: self.dustNode.bounds.width / 2.0, y: self.dustNode.bounds.height / 2.0)) } else { self.blurredImageNode.removeFromSupernode() @@ -452,6 +453,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr private var automaticDownload: InteractiveMediaNodeAutodownloadMode? public var automaticPlayback: Bool? private var preferredStoryHighQuality: Bool = false + private var showSensitiveContent: Bool = false private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) @@ -1575,6 +1577,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticDownload = automaticDownload strongSelf.preferredStoryHighQuality = associatedData.preferredStoryHighQuality + strongSelf.showSensitiveContent = associatedData.showSensitiveContent if let previousArguments = strongSelf.currentImageArguments { if previousArguments.imageSize == arguments.imageSize { @@ -2404,20 +2407,25 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr displaySpoiler = true } else if isSecretMedia { displaySpoiler = true - } else if message.isAgeRestricted() { - displaySpoiler = true - icon = .eye + } else if message.isSensitiveContent(platform: "ios") { + if !self.showSensitiveContent { + displaySpoiler = true + icon = .eye + } } - if displaySpoiler { - if self.extendedMediaOverlayNode == nil, let context = self.context { + if displaySpoiler, let context = self.context { + let extendedMediaOverlayNode: ExtendedMediaOverlayNode + if let current = self.extendedMediaOverlayNode { + extendedMediaOverlayNode = current + } else { let enableAnimations = context.sharedContext.energyUsageSettings.fullTranslucency && !isPreview - let extendedMediaOverlayNode = ExtendedMediaOverlayNode(context: context, hasImageOverlay: !isSecretMedia, icon: icon, enableAnimations: enableAnimations) + extendedMediaOverlayNode = ExtendedMediaOverlayNode(context: context, hasImageOverlay: !isSecretMedia, icon: icon, enableAnimations: enableAnimations) extendedMediaOverlayNode.tapped = { [weak self] in guard let self else { return } - if message.isAgeRestricted() { + if message.isSensitiveContent(platform: "ios") { self.activateAgeRestrictedMedia?() } else { self.internallyVisible = true @@ -2428,7 +2436,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr self.extendedMediaOverlayNode = extendedMediaOverlayNode self.pinchContainerNode.contentNode.insertSubnode(extendedMediaOverlayNode, aboveSubnode: self.imageNode) } - self.extendedMediaOverlayNode?.frame = self.imageNode.frame + extendedMediaOverlayNode.frame = self.imageNode.frame var tappable = false if !isSecretMedia { @@ -2439,13 +2447,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr break } } - - self.extendedMediaOverlayNode?.isUserInteractionEnabled = tappable + extendedMediaOverlayNode.isUserInteractionEnabled = tappable var viewText: String = "" - if message.isAgeRestricted() { - //TODO:localize - viewText = "18+ Content" + if case .eye = icon { + viewText = strings.Chat_SensitiveContent + extendedMediaOverlayNode.dustNode.revealOnTap = false } else { outer: for attribute in message.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute { @@ -2460,8 +2467,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr break } } + extendedMediaOverlayNode.dustNode.revealOnTap = true } - self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: viewText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners) + extendedMediaOverlayNode.update(size: self.imageNode.frame.size, text: viewText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: extendedMediaOverlayNode.view), corners: self.currentImageArguments?.corners) } else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { self.extendedMediaOverlayNode = nil extendedMediaOverlayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak extendedMediaOverlayNode] _ in @@ -2669,12 +2677,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } public func ignoreTapActionAtPoint(_ point: CGPoint) -> Bool { -// if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { -// let convertedPoint = self.view.convert(point, to: extendedMediaOverlayNode.view) -// if extendedMediaOverlayNode.buttonNode.frame.contains(convertedPoint) { -// return true -// } -// } + if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { + let convertedPoint = self.view.convert(point, to: extendedMediaOverlayNode.view) + if extendedMediaOverlayNode.buttonNode.frame.contains(convertedPoint) { + return true + } + } return false } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 91e3e2c5378..7d92e117b03 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -348,8 +348,15 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } var hasAvatar = false - if !hasActionMedia && !isBroadcastChannel { - hasAvatar = true + if !hasActionMedia { + if !isBroadcastChannel { + hasAvatar = true + } else if let channel = message.peers[message.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, message.author?.id != channel.id { + if info.flags.contains(.messagesShouldHaveProfiles) { + hasAvatar = true + effectiveAuthor = message.author + } + } } if hasAvatar { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index 96b9be4346e..430dbd7bf96 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -159,6 +159,15 @@ public final class MessageReactionButtonsNode: ASDisplayNode { } case let .custom(fileId): animationFileId = fileId + case .stars: + if let availableReactions = availableReactions { + for availableReaction in availableReactions.reactions { + if availableReaction.value == reaction.value { + centerAnimation = availableReaction.centerAnimation + break + } + } + } } var peers: [EnginePeer] = [] @@ -530,7 +539,7 @@ public final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleConte } return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in - let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + let reactionsAttribute = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) let buttonsUpdate = buttonsNode.prepareUpdate( context: item.context, presentationData: item.presentationData, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index 5dd68008e51..ec3f7e17c19 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -407,6 +407,8 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { return .builtin(value) case let .custom(fileId): return .custom(fileId: fileId, file: nil) + case .stars: + return .stars } } if let selectionState = presentationInterfaceState.interfaceState.selectionState { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 926eb1419ca..cdcaf4d6007 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -839,9 +839,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { let reactions: ReactionsMessageAttribute if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) { - reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } else { - reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: []) + reactions = mergedMessageReactions(attributes: item.message.attributes, isTags: item.message.areReactionsTags(accountPeerId: item.context.account.peerId)) ?? ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 2055bbcf126..2be9d0c8c51 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -754,7 +754,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } strongSelf.textAccessibilityOverlayNode.frame = textFrame - //TODO:localize + //TODO:release //strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout strongSelf.updateIsTranslating(isTranslating) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index a869fe15437..49875649c4b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -82,7 +82,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } } } - let openChatMessageMode: ChatControllerInteractionOpenMessageMode + var openChatMessageMode: ChatControllerInteractionOpenMessageMode switch mode { case .default: openChatMessageMode = .default @@ -91,6 +91,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent case .automaticPlayback: openChatMessageMode = .automaticPlayback } + if let adAttribute = item.message.adAttribute, adAttribute.hasContentMedia { + openChatMessageMode = .automaticPlayback + } if !item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode)) { if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { var isConcealed = true @@ -510,6 +513,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } actionTitle = adAttribute.buttonText.uppercased() + if !isTelegramMeLink(adAttribute.url) { + actionIcon = .link + } displayLine = true } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 3f766b0913e..439daa1dbea 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -22,6 +22,8 @@ public final class ChatRecentActionsController: TelegramBaseController { private let context: AccountContext private let peer: Peer private let initialAdminPeerId: PeerId? + let starsState: StarsRevenueStats? + private var presentationData: PresentationData private var presentationDataPromise = Promise() override public var updatedPresentationData: (PresentationData, Signal) { @@ -37,10 +39,11 @@ public final class ChatRecentActionsController: TelegramBaseController { private var adminsDisposable: Disposable? - public init(context: AccountContext, peer: Peer, adminPeerId: PeerId?) { + public init(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) { self.context = context self.peer = peer self.initialAdminPeerId = adminPeerId + self.starsState = starsState self.presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 32d3549ad1f..335f76f486a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -210,10 +210,15 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { ])]) strongSelf.presentController(actionSheet, .window(.root), nil) } else { - let controller = inviteLinkEditController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peerId: peer.id, invite: invite, completion: { [weak self] _ in - self?.eventLogContext.reload() - }) - controller.navigationPresentation = .modal + let controller = InviteLinkViewController( + context: strongSelf.context, + updatedPresentationData: strongSelf.controller?.updatedPresentationData, + peerId: peer.id, + invite: invite, + invitationsContext: nil, + revokedInvitationsContext: nil, + importersContext: nil + ) strongSelf.pushController(controller) } return true @@ -233,7 +238,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { let gallerySource = GalleryControllerItemSource.standaloneMessage(message, nil) return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: true, reverseMessageGalleryOrder: false, navigationController: navigationController, dismissInput: { //self?.chatDisplayNode.dismissInput() - }, present: { c, a in + }, present: { c, a, _ in self?.presentController(c, .window(.root), a) }, transitionNode: { messageId, media, adjustRect in var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? @@ -628,6 +633,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift index f593ba23785..7a597f83b0f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift @@ -442,7 +442,7 @@ public func channelRecentActionsFilterController(context: AccountContext, update antiSpamBotPeerPromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: antiSpamBotId)) |> map { peer in if let peer = peer, case let .user(user) = peer { - return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: user) + return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: user) } else { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 7126d2d6208..ea5afffba04 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -87,7 +87,7 @@ private func filterOriginalMessageFlags(_ message: Message) -> Message { private func filterMessageChannelPeer(_ peer: Peer) -> Peer { if let peer = peer as? TelegramChannel { - return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId, emojiStatus: peer.emojiStatus, approximateBoostLevel: peer.approximateBoostLevel) + return TelegramChannel(id: peer.id, accessHash: peer.accessHash, title: peer.title, username: peer.username, photo: peer.photo, creationDate: peer.creationDate, version: peer.version, participationStatus: peer.participationStatus, info: .group(TelegramChannelGroupInfo(flags: [])), flags: peer.flags, restrictionInfo: peer.restrictionInfo, adminRights: peer.adminRights, bannedRights: peer.bannedRights, defaultBannedRights: peer.defaultBannedRights, usernames: peer.usernames, storiesHidden: peer.storiesHidden, nameColor: peer.nameColor, backgroundEmojiId: peer.backgroundEmojiId, profileColor: peer.profileColor, profileBackgroundEmojiId: peer.profileBackgroundEmojiId, emojiStatus: peer.emojiStatus, approximateBoostLevel: peer.approximateBoostLevel, subscriptionUntilDate: peer.subscriptionUntilDate) } return peer } @@ -347,7 +347,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) - case let .toggleSignatures(value): + case .toggleSignatures(let value), .toggleSignatureProfiles(let value): var peers = SimpleDictionary() var author: Peer? if let peer = self.entry.peers[self.entry.event.peerId] { @@ -357,14 +357,28 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] if value { - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleSignaturesOn(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + let pattern: (String) -> PresentationStrings.FormattedString + if case .toggleSignatureProfiles = self.entry.event.action { + pattern = self.presentationData.strings.Channel_AdminLog_MessageToggleProfileSignaturesOn + } else { + pattern = self.presentationData.strings.Channel_AdminLog_MessageToggleSignaturesOn + } + + appendAttributedText(text: pattern(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in if index == 0, let author = author { return [.TextMention(peerId: author.id)] } return [] }, to: &text, entities: &entities) } else { - appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleSignaturesOff(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + let pattern: (String) -> PresentationStrings.FormattedString + if case .toggleSignatureProfiles = self.entry.event.action { + pattern = self.presentationData.strings.Channel_AdminLog_MessageToggleProfileSignaturesOff + } else { + pattern = self.presentationData.strings.Channel_AdminLog_MessageToggleSignaturesOff + } + + appendAttributedText(text: pattern(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in if index == 0, let author = author { return [.TextMention(peerId: author.id)] } @@ -690,8 +704,8 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { isBroadcast = false } - if case let .member(_, _, _, prevBanInfo, _) = prev.participant { - if case let .member(_, _, _, newBanInfo, _) = new.participant { + if case let .member(_, _, _, prevBanInfo, _, _) = prev.participant { + if case let .member(_, _, _, newBanInfo, _, _) = new.participant { let newFlags = newBanInfo?.rights.flags ?? [] var addedRights = newBanInfo?.rights.flags ?? [] @@ -876,8 +890,8 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } } } - } else if case let .member(_, _, prevAdminRights, _, prevRank) = prev.participant { - if case let .member(_, _, newAdminRights, _, newRank) = new.participant { + } else if case let .member(_, _, prevAdminRights, _, prevRank, _) = prev.participant { + if case let .member(_, _, newAdminRights, _, newRank, _) = new.participant { var prevFlags = prevAdminRights?.rights.rights ?? [] var newFlags = newAdminRights?.rights.rights ?? [] @@ -1460,7 +1474,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_DeletedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_DeletedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") appendAttributedText(text: rawText, generateEntities: { index in if index == 0, let author = author { @@ -1486,7 +1500,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_RevokedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_RevokedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") appendAttributedText(text: rawText, generateEntities: { index in if index == 0, let author = author { @@ -1512,7 +1526,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var text: String = "" var entities: [MessageTextEntity] = [] - let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_EditedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", updatedInvite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_EditedInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", updatedInvite.link?.replacingOccurrences(of: "https://", with: "") ?? "") appendAttributedText(text: rawText, generateEntities: { index in if index == 0, let author = author { @@ -1540,9 +1554,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let rawText: PresentationStrings.FormattedString if joinedViaFolderLink { - rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaFolderInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaFolderInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") } else { - rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link_?.replacingOccurrences(of: "https://", with: "") ?? "") + rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "") } appendAttributedText(text: rawText, generateEntities: { index in @@ -1636,6 +1650,8 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { reactionText = "." entities.append(MessageTextEntity(range: (text as NSString).length ..< (text as NSString).length + (reactionText as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: fileId))) text.append(reactionText) + case .stars: + break } } } @@ -1709,7 +1725,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let rawText: PresentationStrings.FormattedString switch invite { - case let .link(link, _, _, _, _, _, _, _, _, _, _, _): + case let .link(link, _, _, _, _, _, _, _, _, _, _, _, _): rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaRequest(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", link.replacingOccurrences(of: "https://", with: ""), approver.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "") case .publicJoinRequest: rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaPublicRequest(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", approver.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "") @@ -2326,14 +2342,3 @@ func chatRecentActionsHistoryPreparedTransition(from fromEntries: [ChatRecentAct return ChatRecentActionsHistoryTransition(filteredEntries: toEntries, type: type, deletions: deletions, insertions: insertions, updates: updates, canLoadEarlier: canLoadEarlier, displayingResults: displayingResults, searchResultsState: searchResultsState, synchronous: !toggledDeletedMessageIds.isEmpty, isEmpty: toEntries.isEmpty) } - -private extension ExportedInvitation { - var link_: String? { - switch self { - case let .link(link, _, _, _, _, _, _, _, _, _, _, _): - return link - case .publicJoinRequest: - return nil - } - } -} diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index e07264e9c3e..714ce82b874 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -492,6 +492,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD index 5d003e453bc..1cbadf2f427 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD @@ -31,6 +31,8 @@ swift_library( "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", "//submodules/AvatarNode", "//submodules/Components/BundleIconComponent", + "//submodules/CheckNode", + "//submodules/TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 66cb841f9c2..2085326af06 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -18,6 +18,8 @@ import SliderComponent import RoundedRectWithTailPath import AvatarNode import BundleIconComponent +import CheckNode +import TextFormat private final class BalanceComponent: CombinedComponent { let context: AccountContext @@ -56,15 +58,14 @@ private final class BalanceComponent: CombinedComponent { static var body: Body { let title = Child(MultilineTextComponent.self) let balance = Child(MultilineTextComponent.self) - let icon = Child(EmojiStatusComponent.self) + let icon = Child(BundleIconComponent.self) return { context in var size = CGSize(width: 0.0, height: 0.0) - //TODO:localize let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: "Balance", font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: context.component.strings.SendStarReactions_Balance, font: Font.regular(14.0), textColor: context.component.theme.list.itemPrimaryTextColor)) ), availableSize: context.availableSize, transition: .immediate @@ -89,19 +90,9 @@ private final class BalanceComponent: CombinedComponent { let iconSize = CGSize(width: 18.0, height: 18.0) let icon = icon.update( - component: EmojiStatusComponent( - context: context.component.context, - animationCache: context.component.context.animationCache, - animationRenderer: context.component.context.animationRenderer, - content: .animation( - content: .customEmoji(fileId: MessageReaction.starsReactionId), - size: iconSize, - placeholderColor: .gray, - themeColor: nil, - loopMode: .count(0) - ), - isVisibleForAnimations: true, - action: nil + component: BundleIconComponent( + name: "Premium/Stars/StarLarge", + tintColor: nil ), availableSize: iconSize, transition: context.transition @@ -127,7 +118,7 @@ private final class BalanceComponent: CombinedComponent { ) context.add( icon.position( - icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: title.size.height + titleSpacing), size: icon.size)).center + icon.size.centered(in: CGRect(origin: CGPoint(x: -1.0, y: title.size.height + titleSpacing), size: icon.size)).center ) ) @@ -174,7 +165,7 @@ private final class BadgeComponent: Component { private let badgeShapeLayer = SimpleShapeLayer() private let badgeForeground: SimpleLayer - private let badgeIcon: UIImageView + let badgeIcon: UIImageView private let badgeLabel: BadgeLabelView private let badgeLabelMaskView = UIImageView() @@ -479,18 +470,21 @@ private final class PeerComponent: Component { let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings - let peer: EnginePeer + let peer: EnginePeer? + let count: Int init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, - peer: EnginePeer + peer: EnginePeer?, + count: Int ) { self.context = context self.theme = theme self.strings = strings self.peer = peer + self.count = count } static func ==(lhs: PeerComponent, rhs: PeerComponent) -> Bool { @@ -506,6 +500,9 @@ private final class PeerComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.count != rhs.count { + return false + } return true } @@ -539,14 +536,18 @@ private final class PeerComponent: Component { let avatarSize = CGSize(width: 60.0, height: 60.0) let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: avatarSize) avatarNode.frame = avatarFrame - avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + if let peer = component.peer { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, synchronousLoad: true) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: nil, overrideImage: .anonymousSavedMessagesIcon(isColored: false), synchronousLoad: true) + } avatarNode.updateSize(size: avatarFrame.size) let badgeSize = self.badge.update( transition: .immediate, component: AnyComponent(PeerBadgeComponent( theme: component.theme, - title: "800" + title: "\(component.count)" )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) @@ -561,10 +562,17 @@ private final class PeerComponent: Component { let titleSpacing: CGFloat = 8.0 + let peerTitle: String + if let peer = component.peer { + peerTitle = peer.compactDisplayTitle + } else { + peerTitle = component.strings.SendStarReactions_UserLabelAnonymous + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: peerTitle, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: avatarSize.width + 10.0 * 2.0, height: 100.0) @@ -590,26 +598,272 @@ private final class PeerComponent: Component { } } +private final class SliderBackgroundComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let value: CGFloat + let topCutoff: CGFloat? + + init( + theme: PresentationTheme, + strings: PresentationStrings, + value: CGFloat, + topCutoff: CGFloat? + ) { + self.theme = theme + self.strings = strings + self.value = value + self.topCutoff = topCutoff + } + + static func ==(lhs: SliderBackgroundComponent, rhs: SliderBackgroundComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.topCutoff != rhs.topCutoff { + return false + } + return true + } + + private enum TopTextOverflowState { + case left + case center + case right + + func animates(from: TopTextOverflowState) -> Bool { + switch self { + case .left: + return false + case .center: + switch from { + case .left: + return false + case .center: + return false + case .right: + return true + } + case .right: + switch from { + case .left: + return false + case .center: + return true + case .right: + return false + } + } + } + } + + final class View: UIView { + private let sliderBackground = UIView() + private let sliderForeground = UIView() + private let sliderStars = SliderStarsView() + + private let topForegroundLine = SimpleLayer() + private let topBackgroundLine = SimpleLayer() + private let topForegroundText = ComponentView() + private let topBackgroundText = ComponentView() + + private var topTextOverflowState: TopTextOverflowState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.sliderBackground.clipsToBounds = true + + self.sliderForeground.clipsToBounds = true + self.sliderForeground.addSubview(self.sliderStars) + + self.addSubview(self.sliderBackground) + self.addSubview(self.sliderForeground) + + self.sliderBackground.layer.addSublayer(self.topBackgroundLine) + self.sliderForeground.layer.addSublayer(self.topForegroundLine) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SliderBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.sliderBackground.backgroundColor = component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(component.theme.overallDarkAppearance ? 0.2 : 0.07) + self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D) + self.topForegroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor + self.topBackgroundLine.backgroundColor = component.theme.list.plainBackgroundColor.cgColor + + transition.setFrame(view: self.sliderBackground, frame: CGRect(origin: CGPoint(), size: availableSize)) + + let sliderMinWidth = availableSize.height + let sliderAreaWidth: CGFloat = availableSize.width - sliderMinWidth + let sliderForegroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * component.value), height: availableSize.height)) + transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame) + + self.sliderBackground.layer.cornerRadius = availableSize.height * 0.5 + self.sliderForeground.layer.cornerRadius = availableSize.height * 0.5 + + self.sliderStars.frame = CGRect(origin: .zero, size: availableSize) + self.sliderStars.update(size: availableSize, value: component.value) + + self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth + + let topCutoff = component.topCutoff ?? 0.0 + + let topX = floorToScreenPixels(sliderAreaWidth * topCutoff) + let topLineAvoidDistance = 6.0 + let knobWidth: CGFloat = 30.0 + let topLineClosestEdge = min(abs(sliderForegroundFrame.maxX - topX), abs(sliderForegroundFrame.maxX - knobWidth - topX)) + var topLineOverlayFactor = topLineClosestEdge / topLineAvoidDistance + topLineOverlayFactor = max(0.0, min(1.0, topLineOverlayFactor)) + if sliderForegroundFrame.maxX - knobWidth <= topX && sliderForegroundFrame.maxX >= topX { + topLineOverlayFactor = 0.0 + } + + let topLineHeight: CGFloat = availableSize.height + let topLineAlpha: CGFloat = topLineOverlayFactor * topLineOverlayFactor + + let topLineFrameTransition = transition + let topLineAlphaTransition = transition + /*if transition.userData(ChatSendStarsScreenComponent.IsAdjustingAmountHint.self) != nil { + topLineFrameTransition = .easeInOut(duration: 0.12) + topLineAlphaTransition = .easeInOut(duration: 0.12) + }*/ + + let topLineFrame = CGRect(origin: CGPoint(x: topX, y: (availableSize.height - topLineHeight) * 0.5), size: CGSize(width: 1.0, height: topLineHeight)) + + topLineFrameTransition.setFrame(layer: self.topForegroundLine, frame: topLineFrame) + topLineAlphaTransition.setAlpha(layer: self.topForegroundLine, alpha: topLineAlpha) + topLineFrameTransition.setFrame(layer: self.topBackgroundLine, frame: topLineFrame) + topLineAlphaTransition.setAlpha(layer: self.topBackgroundLine, alpha: topLineAlpha) + + let topTextSize = self.topForegroundText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.SendStarReactions_SliderTop, font: Font.semibold(15.0), textColor: UIColor(white: 1.0, alpha: 0.4))) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let _ = self.topBackgroundText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.SendStarReactions_SliderTop, font: Font.semibold(15.0), textColor: component.theme.overallDarkAppearance ? UIColor(white: 1.0, alpha: 0.22) : UIColor(white: 0.0, alpha: 0.2))) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + var topTextFrame = CGRect(origin: CGPoint(x: topLineFrame.maxX + 6.0, y: floor((availableSize.height - topTextSize.height) * 0.5)), size: topTextSize) + + let topTextFrameTransition = transition + + let topTextLeftInset: CGFloat = 4.0 + var topTextOverflowWidth: CGFloat = 0.0 + let topTextOverflowState: TopTextOverflowState + if sliderForegroundFrame.maxX < topTextFrame.minX - topTextLeftInset { + topTextOverflowState = .left + } else if sliderForegroundFrame.maxX >= topTextFrame.minX - topTextLeftInset && sliderForegroundFrame.maxX - knobWidth < topTextFrame.maxX + topTextLeftInset { + topTextOverflowWidth = sliderForegroundFrame.maxX - (topTextFrame.minX - topTextLeftInset) + topTextOverflowState = .center + } else { + topTextOverflowState = .right + } + + topTextFrame.origin.x += topTextOverflowWidth + + if let topForegroundTextView = self.topForegroundText.view, let topBackgroundTextView = self.topBackgroundText.view { + if topForegroundTextView.superview == nil { + topBackgroundTextView.layer.anchorPoint = CGPoint() + self.sliderBackground.addSubview(topBackgroundTextView) + + topForegroundTextView.layer.anchorPoint = CGPoint() + self.sliderForeground.addSubview(topForegroundTextView) + } + + var animateTopTextAdditionalX: CGFloat = 0.0 + if transition.userData(ChatSendStarsScreenComponent.IsAdjustingAmountHint.self) != nil { + if let previousState = self.topTextOverflowState, previousState != topTextOverflowState, topTextOverflowState.animates(from: previousState) { + animateTopTextAdditionalX = topForegroundTextView.center.x - topTextFrame.origin.x + } + } + + topTextFrameTransition.setPosition(view: topForegroundTextView, position: topTextFrame.origin) + topTextFrameTransition.setPosition(view: topBackgroundTextView, position: topTextFrame.origin) + + topForegroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size) + topBackgroundTextView.bounds = CGRect(origin: CGPoint(), size: topTextFrame.size) + + if animateTopTextAdditionalX != 0.0 { + topForegroundTextView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: animateTopTextAdditionalX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.3, damping: 100.0, additive: true) + topBackgroundTextView.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: animateTopTextAdditionalX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.3, damping: 100.0, additive: true) + } + + topForegroundTextView.isHidden = component.topCutoff == nil + topBackgroundTextView.isHidden = topForegroundTextView.isHidden + self.topBackgroundLine.isHidden = topX < 10.0 + self.topForegroundLine.isHidden = self.topBackgroundLine.isHidden + } + self.topTextOverflowState = topTextOverflowState + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + private final class ChatSendStarsScreenComponent: Component { + final class IsAdjustingAmountHint { + } + typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peer: EnginePeer + let myPeer: EnginePeer + let messageId: EngineMessage.Id + let maxAmount: Int let balance: Int64? - let topPeers: [EnginePeer] - let completion: (Int64) -> Void + let currentSentAmount: Int? + let topPeers: [ChatSendStarsScreen.TopPeer] + let myTopPeer: ChatSendStarsScreen.TopPeer? + let completion: (Int64, Bool, Bool, ChatSendStarsScreen.TransitionOut) -> Void init( context: AccountContext, peer: EnginePeer, + myPeer: EnginePeer, + messageId: EngineMessage.Id, + maxAmount: Int, balance: Int64?, - topPeers: [EnginePeer], - completion: @escaping (Int64) -> Void + currentSentAmount: Int?, + topPeers: [ChatSendStarsScreen.TopPeer], + myTopPeer: ChatSendStarsScreen.TopPeer?, + completion: @escaping (Int64, Bool, Bool, ChatSendStarsScreen.TransitionOut) -> Void ) { self.context = context self.peer = peer + self.myPeer = myPeer + self.messageId = messageId + self.maxAmount = maxAmount self.balance = balance + self.currentSentAmount = currentSentAmount self.topPeers = topPeers + self.myTopPeer = myTopPeer self.completion = completion } @@ -620,12 +874,24 @@ private final class ChatSendStarsScreenComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.myPeer != rhs.myPeer { + return false + } + if lhs.maxAmount != rhs.maxAmount { + return false + } if lhs.balance != rhs.balance { return false } + if lhs.currentSentAmount != rhs.currentSentAmount { + return false + } if lhs.topPeers != rhs.topPeers { return false } + if lhs.myTopPeer != rhs.myTopPeer { + return false + } return true } @@ -664,10 +930,8 @@ private final class ChatSendStarsScreenComponent: Component { private let descriptionText = ComponentView() private let badgeStars = BadgeStarsView() + private let sliderBackground = ComponentView() private let slider = ComponentView() - private let sliderBackground = UIView() - private let sliderForeground = UIView() - private let sliderStars = SliderStarsView() private let badge = ComponentView() private var topPeersLeftSeparator: SimpleLayer? @@ -675,7 +939,10 @@ private final class ChatSendStarsScreenComponent: Component { private var topPeersTitleBackground: SimpleLayer? private var topPeersTitle: ComponentView? - private var topPeerItems: [EnginePeer.Id: ComponentView] = [:] + private var anonymousSeparator = SimpleLayer() + private var anonymousContents = ComponentView() + + private var topPeerItems: [ChatSendStarsScreen.TopPeer.Id: ComponentView] = [:] private let actionButton = ComponentView() private let buttonDescriptionText = ComponentView() @@ -691,10 +958,16 @@ private final class ChatSendStarsScreenComponent: Component { private var topOffsetDistance: CGFloat? + private var balance: Int64? private var amount: Int64 = 1 + private var isAnonymous: Bool = false private var cachedStarImage: (UIImage, PresentationTheme)? private var cachedCloseImage: UIImage? + private var isPastTopCutoff: Bool? + + private var balanceDisposable: Disposable? + override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 @@ -740,9 +1013,6 @@ private final class ChatSendStarsScreenComponent: Component { self.scrollView.addSubview(self.scrollContentView) - self.sliderForeground.clipsToBounds = true - self.sliderForeground.addSubview(self.sliderStars) - self.addSubview(self.navigationBarContainer) self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) @@ -752,6 +1022,10 @@ private final class ChatSendStarsScreenComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.balanceDisposable?.dispose() + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) @@ -860,7 +1134,26 @@ private final class ChatSendStarsScreenComponent: Component { let sideInset: CGFloat = 16.0 if self.component == nil { - self.amount = 1 + self.balance = component.balance + self.amount = 50 + if let myTopPeer = component.myTopPeer { + self.isAnonymous = myTopPeer.isAnonymous + } + + if let starsContext = component.context.starsContext { + self.balanceDisposable = (starsContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + guard let self else { + return + } + if let state { + if self.balance != state.balance { + self.balance = state.balance + self.state?.updated(transition: .immediate) + } + } + }) + } } self.component = component @@ -889,21 +1182,21 @@ private final class ChatSendStarsScreenComponent: Component { let sliderSize = self.slider.update( transition: transition, component: AnyComponent(SliderComponent( - valueCount: 1000, - value: 0, + valueCount: component.maxAmount, + value: Int(self.amount), markPositions: false, trackBackgroundColor: .clear, trackForegroundColor: .clear, knobSize: 26.0, knobColor: .white, valueUpdated: { [weak self] value in - guard let self else { + guard let self, let component = self.component else { return } self.amount = 1 + Int64(value) - self.state?.updated(transition: .immediate) + self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(IsAdjustingAmountHint())) - let sliderValue = Float(value) / 1000.0 + let sliderValue = Float(value) / Float(component.maxAmount) let currentTimestamp = CACurrentMediaTime() if let previousTimestamp { @@ -955,34 +1248,58 @@ private final class ChatSendStarsScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sliderInset * 2.0, height: 30.0) ) let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize) - if let sliderView = self.slider.view { + let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0)) + + let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(component.maxAmount - 1) + + let topOthersCount: Int? = component.topPeers.filter({ !$0.isMy }).max(by: { $0.count < $1.count })?.count + var topCount: Int? + if let topOthersCount { + if let myTopPeer = component.myTopPeer { + topCount = max(0, topOthersCount - myTopPeer.count + 1) + } else { + topCount = topOthersCount + } + if topCount == 0 { + topCount = nil + } + } + + var topCutoffFraction: CGFloat? + if let topCount { + let topCutoffFractionValue = CGFloat(topCount) / CGFloat(component.maxAmount - 1) + topCutoffFraction = topCutoffFractionValue + + let isPastCutoff = progressFraction >= topCutoffFractionValue + if let isPastTopCutoff = self.isPastTopCutoff, isPastTopCutoff != isPastCutoff { + HapticFeedback().tap() + } + self.isPastTopCutoff = isPastCutoff + } else { + self.isPastTopCutoff = nil + } + + let _ = self.sliderBackground.update( + transition: transition, + component: AnyComponent(SliderBackgroundComponent( + theme: environment.theme, + strings: environment.strings, + value: progressFraction, + topCutoff: topCutoffFraction + )), + environment: {}, + containerSize: sliderBackgroundFrame.size + ) + + if let sliderView = self.slider.view, let sliderBackgroundView = self.sliderBackground.view { if sliderView.superview == nil { self.scrollContentView.addSubview(self.badgeStars) - self.scrollContentView.addSubview(self.sliderBackground) - self.scrollContentView.addSubview(self.sliderForeground) + self.scrollContentView.addSubview(sliderBackgroundView) self.scrollContentView.addSubview(sliderView) } transition.setFrame(view: sliderView, frame: sliderFrame) - self.sliderBackground.backgroundColor = UIColor(rgb: 0xEEEEEF) - self.sliderForeground.backgroundColor = UIColor(rgb: 0xFFB10D) - - let sliderBackgroundFrame = CGRect(origin: CGPoint(x: sliderFrame.minX - 8.0, y: sliderFrame.minY + 7.0), size: CGSize(width: sliderFrame.width + 16.0, height: sliderFrame.height - 14.0)) - transition.setFrame(view: self.sliderBackground, frame: sliderBackgroundFrame) - - let progressFraction: CGFloat = CGFloat(self.amount) / CGFloat(1000 - 1) - let sliderMinWidth = sliderBackgroundFrame.height - let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth - let sliderForegroundFrame = CGRect(origin: CGPoint(x: sliderBackgroundFrame.minX, y: sliderBackgroundFrame.minY), size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) - transition.setFrame(view: self.sliderForeground, frame: sliderForegroundFrame) - - self.sliderBackground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 - self.sliderForeground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 - - self.sliderStars.frame = CGRect(origin: .zero, size: sliderBackgroundFrame.size) - self.sliderStars.update(size: sliderBackgroundFrame.size, value: progressFraction) - - self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth + transition.setFrame(view: sliderBackgroundView, frame: sliderBackgroundFrame) var effectiveInertiaDirection = self.inertiaDirection if progressFraction <= 0.03 || progressFraction >= 0.97 { @@ -999,6 +1316,11 @@ private final class ChatSendStarsScreenComponent: Component { environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) + + let sliderMinWidth = sliderBackgroundFrame.height + let sliderAreaWidth: CGFloat = sliderBackgroundFrame.width - sliderMinWidth + let sliderForegroundFrame = CGRect(origin: sliderBackgroundFrame.origin, size: CGSize(width: sliderMinWidth + floorToScreenPixels(sliderAreaWidth * progressFraction), height: sliderBackgroundFrame.height)) + var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize) if let badgeView = self.badge.view as? BadgeComponent.View { if badgeView.superview == nil { @@ -1036,7 +1358,7 @@ private final class ChatSendStarsScreenComponent: Component { context: component.context, theme: environment.theme, strings: environment.strings, - balance: component.balance + balance: self.balance )), environment: {}, containerSize: CGSize(width: 120.0, height: 100.0) @@ -1094,7 +1416,7 @@ private final class ChatSendStarsScreenComponent: Component { let titleSize = title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "React with Stars", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) @@ -1109,8 +1431,13 @@ private final class ChatSendStarsScreenComponent: Component { contentHeight += 56.0 contentHeight += 8.0 - - let text = "Choose how many stars you want to send to **\(component.peer.debugDisplayTitle)** to support this post." + + let text: String + if let currentSentAmount = component.currentSentAmount { + text = environment.strings.SendStarReactions_TextSentStars(Int32(currentSentAmount)) + } else { + text = environment.strings.SendStarReactions_TextGeneric(component.peer.debugDisplayTitle).string + } let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) @@ -1183,11 +1510,10 @@ private final class ChatSendStarsScreenComponent: Component { topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor - //TODO:localize let topPeersTitleSize = topPeersTitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Top Senders", font: Font.semibold(15.0), textColor: .white)) + text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SectionTop, font: Font.semibold(15.0), textColor: .white)) )), environment: {}, containerSize: CGSize(width: 300.0, height: 100.0) @@ -1212,9 +1538,36 @@ private final class ChatSendStarsScreenComponent: Component { transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) - var validIds: [EnginePeer.Id] = [] + var mappedTopPeers = component.topPeers + if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) { + mappedTopPeers.remove(at: index) + } + var myCount = Int(self.amount) + if let myTopPeer = component.myTopPeer { + myCount += myTopPeer.count + } + mappedTopPeers.append(ChatSendStarsScreen.TopPeer( + peer: self.isAnonymous ? nil : component.myPeer, + isMy: true, + count: myCount + )) + mappedTopPeers.sort(by: { $0.count > $1.count }) + if mappedTopPeers.count > 3 { + mappedTopPeers = Array(mappedTopPeers.prefix(3)) + } + + var animateItems = false + var itemPositionTransition = transition + var itemAlphaTransition = transition + if transition.userData(IsAdjustingAmountHint.self) != nil { + animateItems = true + itemPositionTransition = .spring(duration: 0.3) + itemAlphaTransition = .easeInOut(duration: 0.15) + } + + var validIds: [ChatSendStarsScreen.TopPeer.Id] = [] var items: [(itemView: ComponentView, size: CGSize)] = [] - for topPeer in component.topPeers { + for topPeer in mappedTopPeers { validIds.append(topPeer.id) let itemView: ComponentView @@ -1227,22 +1580,54 @@ private final class ChatSendStarsScreenComponent: Component { let itemSize = itemView.update( transition: .immediate, - component: AnyComponent(PeerComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - peer: topPeer + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(PeerComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: topPeer.peer, + count: topPeer.count + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component, let peer = topPeer.peer else { + return + } + if let peerInfoController = component.context.sharedContext.makePeerInfoController( + context: component.context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + self.environment?.controller()?.push(peerInfoController) + } + }, + isEnabled: topPeer.peer != nil && topPeer.peer?.id != component.context.account.peerId, + animateAlpha: false )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) items.append((itemView, itemSize)) } - var removedIds: [EnginePeer.Id] = [] + var removedIds: [ChatSendStarsScreen.TopPeer.Id] = [] for (id, itemView) in self.topPeerItems { if !validIds.contains(id) { removedIds.append(id) - itemView.view?.removeFromSuperview() + + if animateItems { + if let itemComponentView = itemView.view { + itemPositionTransition.setScale(view: itemComponentView, scale: 0.001) + itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + } else { + itemView.view?.removeFromSuperview() + } } } for id in removedIds { @@ -1262,10 +1647,26 @@ private final class ChatSendStarsScreenComponent: Component { var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing for (itemView, itemSize) in items { if let itemComponentView = itemView.view { + var animateItem = animateItems if itemComponentView.superview == nil { self.scrollContentView.addSubview(itemComponentView) + animateItem = false + ComponentTransition.immediate.setScale(view: itemComponentView, scale: 0.001) + itemComponentView.alpha = 0.0 + } + + let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) + + if animateItem { + itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center) + itemPositionTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + } else { + itemComponentView.center = itemFrame.center + itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) } - itemComponentView.frame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) + + itemPositionTransition.setScale(view: itemComponentView, scale: 1.0) + itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 1.0) } itemX += itemSize.width + itemSpacing } @@ -1273,13 +1674,80 @@ private final class ChatSendStarsScreenComponent: Component { contentHeight += 161.0 } + do { + if !component.topPeers.isEmpty { + contentHeight += 2.0 + } + + if self.anonymousSeparator.superlayer == nil { + self.scrollContentView.layer.addSublayer(self.anonymousSeparator) + } + + self.anonymousSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + + let checkTheme = CheckComponent.Theme( + backgroundColor: environment.theme.list.itemCheckColors.fillColor, + strokeColor: environment.theme.list.itemCheckColors.foregroundColor, + borderColor: environment.theme.list.itemCheckColors.strokeColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ) + let anonymousContentsSize = self.anonymousContents.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent( + theme: checkTheme, + selected: !self.isAnonymous + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_ShowMyselfInTop, font: Font.regular(16.0), textColor: environment.theme.list.itemPrimaryTextColor)) + ))) + ], + spacing: 10.0 + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + self.isAnonymous = !self.isAnonymous + self.state?.updated(transition: .easeInOut(duration: 0.2)) + + if component.myTopPeer != nil { + let _ = component.context.engine.messages.updateStarsReactionIsAnonymous(id: component.messageId, isAnonymous: self.isAnonymous).startStandalone() + } + }, + animateAlpha: false, + animateScale: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + transition.setFrame(layer: self.anonymousSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) + + contentHeight += 21.0 + + let anonymousContentsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - anonymousContentsSize.width) * 0.5), y: contentHeight), size: anonymousContentsSize) + if let anonymousContentsView = self.anonymousContents.view { + if anonymousContentsView.superview == nil { + self.scrollContentView.addSubview(anonymousContentsView) + } + transition.setFrame(view: anonymousContentsView, frame: anonymousContentsFrame) + } + + contentHeight += anonymousContentsSize.height + 27.0 + } + initialContentHeight = contentHeight if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) } - let buttonString = "Send # \(self.amount)" + let buttonString = environment.strings.SendStarReactions_SendButtonTitle("\(self.amount)").string let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) @@ -1306,7 +1774,50 @@ private final class ChatSendStarsScreenComponent: Component { guard let self, let component = self.component else { return } - component.completion(self.amount) + guard let balance = self.balance else { + return + } + + if balance < self.amount { + let _ = (component.context.engine.payments.starsTopUpOptions() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] options in + guard let self, let component = self.component else { + return + } + guard let starsContext = component.context.starsContext else { + return + } + + let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: .transfer(peerId: component.peer.id, requiredStars: self.amount), completion: { result in + let _ = result + //TODO:release + }) + self.environment?.controller()?.push(purchaseScreen) + self.environment?.controller()?.dismiss() + }) + + return + } + + guard let badgeView = self.badge.view as? BadgeComponent.View else { + return + } + let isBecomingTop: Bool + if let topCount { + isBecomingTop = self.amount > topCount + } else { + isBecomingTop = true + } + + component.completion( + self.amount, + self.isAnonymous, + isBecomingTop, + ChatSendStarsScreen.TransitionOut( + sourceView: badgeView.badgeIcon + ) + ) self.environment?.controller()?.dismiss() } )), @@ -1317,16 +1828,30 @@ private final class ChatSendStarsScreenComponent: Component { let buttonDescriptionTextSize = self.buttonDescriptionText.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .markdown(text: "By sending Stars you agree to the [Terms of Service]()", attributes: MarkdownAttributes( + text: .markdown(text: environment.strings.SendStarReactions_TermsOfServiceFooter, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), - linkAttribute: { url in - return ("URL", url) + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) } )), horizontalAlignment: .center, - maximumNumberOfLines: 0 + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + if let controller = self?.environment?.controller(), let navigationController = controller.navigationController as? NavigationController, let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) @@ -1399,35 +1924,111 @@ private final class ChatSendStarsScreenComponent: Component { public class ChatSendStarsScreen: ViewControllerComponentContainer { public final class InitialData { - let peer: EnginePeer - let balance: Int64? - let topPeers: [EnginePeer] + fileprivate let peer: EnginePeer + fileprivate let myPeer: EnginePeer + fileprivate let messageId: EngineMessage.Id + fileprivate let balance: Int64? + fileprivate let currentSentAmount: Int? + fileprivate let topPeers: [ChatSendStarsScreen.TopPeer] + fileprivate let myTopPeer: ChatSendStarsScreen.TopPeer? fileprivate init( peer: EnginePeer, + myPeer: EnginePeer, + messageId: EngineMessage.Id, balance: Int64?, - topPeers: [EnginePeer] + currentSentAmount: Int?, + topPeers: [ChatSendStarsScreen.TopPeer], + myTopPeer: ChatSendStarsScreen.TopPeer? ) { self.peer = peer + self.myPeer = myPeer + self.messageId = messageId self.balance = balance + self.currentSentAmount = currentSentAmount self.topPeers = topPeers + self.myTopPeer = myTopPeer + } + } + + fileprivate final class TopPeer: Equatable { + enum Id: Hashable { + case anonymous + case my + case peer(EnginePeer.Id) + } + + var id: Id { + if self.isMy { + return .my + } else if let peer = self.peer { + return .peer(peer.id) + } else { + return .anonymous + } + } + + var isAnonymous: Bool { + return self.peer == nil + } + + let peer: EnginePeer? + let isMy: Bool + let count: Int + + init(peer: EnginePeer?, isMy: Bool, count: Int) { + self.peer = peer + self.isMy = isMy + self.count = count + } + + static func ==(lhs: TopPeer, rhs: TopPeer) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if lhs.isMy != rhs.isMy { + return false + } + if lhs.count != rhs.count { + return false + } + return true + } + } + + public final class TransitionOut { + public let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView } } private let context: AccountContext + private var didPlayAppearAnimation: Bool = false private var isDismissed: Bool = false private var presenceDisposable: Disposable? - public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64) -> Void) { + public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, Bool, Bool, TransitionOut) -> Void) { self.context = context + var maxAmount = 2500 + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["stars_paid_reaction_amount_max"] as? Double { + maxAmount = Int(value) + } + super.init(context: context, component: ChatSendStarsScreenComponent( context: context, peer: initialData.peer, + myPeer: initialData.myPeer, + messageId: initialData.messageId, + maxAmount: maxAmount, balance: initialData.balance, + currentSentAmount: initialData.currentSentAmount, topPeers: initialData.topPeers, + myTopPeer: initialData.myTopPeer, completion: completion ), navigationBarAppearance: .none) @@ -1449,12 +2050,16 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true - if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { - componentView.animateIn() + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + + if let componentView = self.node.hostView.componentView as? ChatSendStarsScreenComponent.View { + componentView.animateIn() + } } } - public static func initialData(context: AccountContext, peerId: EnginePeer.Id) -> Signal { + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, messageId: EngineMessage.Id, topPeers: [ReactionsMessageAttribute.TopPeer]) -> Signal { let balance: Signal if let starsContext = context.starsContext { balance = starsContext.state @@ -1466,20 +2071,80 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { balance = .single(nil) } + var currentSentAmount: Int? + var myTopPeer: ReactionsMessageAttribute.TopPeer? + if let myPeer = topPeers.first(where: { $0.isMy }) { + myTopPeer = myPeer + currentSentAmount = Int(myPeer.count) + } + + let allPeerIds = topPeers.compactMap(\.peerId) + + var topPeers = topPeers.sorted(by: { $0.count > $1.count }) + if topPeers.count > 3 { + topPeers = Array(topPeers.prefix(3)) + } + return combineLatest( - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + EngineDataMap(allPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ), balance ) - |> map { peer, accountPeer, balance -> InitialData? in - guard let peer, let accountPeer else { + |> map { peerAndTopPeerMap, balance -> InitialData? in + let (peer, myPeer, topPeerMap) = peerAndTopPeerMap + guard let peer, let myPeer else { return nil } return InitialData( peer: peer, + myPeer: myPeer, + messageId: messageId, balance: balance, - topPeers: [accountPeer, peer] + currentSentAmount: currentSentAmount, + topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in + guard let topPeerId = topPeer.peerId else { + return ChatSendStarsScreen.TopPeer( + peer: nil, + isMy: topPeer.isMy, + count: Int(topPeer.count) + ) + } + guard let topPeerValue = topPeerMap[topPeerId] else { + return nil + } + guard let topPeerValue else { + return nil + } + return ChatSendStarsScreen.TopPeer( + peer: topPeer.isAnonymous ? nil : topPeerValue, + isMy: topPeer.isMy, + count: Int(topPeer.count) + ) + }, + myTopPeer: myTopPeer.flatMap { topPeer -> ChatSendStarsScreen.TopPeer? in + guard let topPeerId = topPeer.peerId else { + return ChatSendStarsScreen.TopPeer( + peer: nil, + isMy: topPeer.isMy, + count: Int(topPeer.count) + ) + } + guard let topPeerValue = topPeerMap[topPeerId] else { + return nil + } + guard let topPeerValue else { + return nil + } + return ChatSendStarsScreen.TopPeer( + peer: topPeer.isAnonymous ? nil : topPeerValue, + isMy: topPeer.isMy, + count: Int(topPeer.count) + ) + } ) } } @@ -1715,3 +2380,97 @@ private final class SliderStarsView: UIView { self.emitterLayer.emitterSize = size } } + +private final class CheckComponent: Component { + struct Theme: Equatable { + public let backgroundColor: UIColor + public let strokeColor: UIColor + public let borderColor: UIColor + public let overlayBorder: Bool + public let hasInset: Bool + public let hasShadow: Bool + public let filledBorder: Bool + public let borderWidth: CGFloat? + + public init(backgroundColor: UIColor, strokeColor: UIColor, borderColor: UIColor, overlayBorder: Bool, hasInset: Bool, hasShadow: Bool, filledBorder: Bool = false, borderWidth: CGFloat? = nil) { + self.backgroundColor = backgroundColor + self.strokeColor = strokeColor + self.borderColor = borderColor + self.overlayBorder = overlayBorder + self.hasInset = hasInset + self.hasShadow = hasShadow + self.filledBorder = filledBorder + self.borderWidth = borderWidth + } + + var checkNodeTheme: CheckNodeTheme { + return CheckNodeTheme( + backgroundColor: self.backgroundColor, + strokeColor: self.strokeColor, + borderColor: self.borderColor, + overlayBorder: self.overlayBorder, + hasInset: self.hasInset, + hasShadow: self.hasShadow, + filledBorder: self.filledBorder, + borderWidth: self.borderWidth + ) + } + } + + let theme: Theme + let selected: Bool + + init( + theme: Theme, + selected: Bool + ) { + self.theme = theme + self.selected = selected + } + + static func ==(lhs: CheckComponent, rhs: CheckComponent) -> Bool { + if lhs.theme != rhs.theme { + return false + } + if lhs.selected != rhs.selected { + return false + } + return true + } + + final class View: UIView { + private var currentValue: CGFloat? + private var animator: DisplayLinkAnimator? + + private var checkLayer: CheckLayer { + return self.layer as! CheckLayer + } + + override class var layerClass: AnyClass { + return CheckLayer.self + } + + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: CheckComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.checkLayer.setSelected(component.selected, animated: true) + self.checkLayer.theme = component.theme.checkNodeTheme + + return CGSize(width: 22.0, height: 22.0) + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift index f2e78e7f465..555f5a2dce6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift @@ -100,6 +100,13 @@ public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditio } case let .custom(_, fileValue): file = fileValue + case .stars: + for reaction in availableReactions.reactions { + if reaction.value == updateReaction.reaction { + file = reaction.centerAnimation + break + } + } } guard let file else { diff --git a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift index 869462e11b7..d35e761b468 100644 --- a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift +++ b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift @@ -10,13 +10,13 @@ public enum AllowedReactions { case all } -public func peerMessageAllowedReactions(context: AccountContext, message: Message) -> Signal { +public func peerMessageAllowedReactions(context: AccountContext, message: Message) -> Signal<(allowedReactions: AllowedReactions?, areStarsEnabled: Bool), NoError> { if message.id.peerId == context.account.peerId { - return .single(.all) + return .single((.all, false)) } if message.containsSecretMedia { - return .single(AllowedReactions.set(Set())) + return .single((AllowedReactions.set(Set()), false)) } return combineLatest( @@ -26,7 +26,7 @@ public func peerMessageAllowedReactions(context: AccountContext, message: Messag ), context.engine.stickers.availableReactions() |> take(1) ) - |> map { data, availableReactions -> AllowedReactions? in + |> map { data, availableReactions -> (allowedReactions: AllowedReactions?, areStarsEnabled: Bool) in let (peer, reactionSettings) = data let maxReactionCount: Int @@ -35,35 +35,41 @@ public func peerMessageAllowedReactions(context: AccountContext, message: Messag } else { maxReactionCount = 11 } + + var areStarsEnabled: Bool = false + if let value = reactionSettings.knownValue?.starsAllowed { + areStarsEnabled = value + } + if let effectiveReactions = message.effectiveReactions(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)), effectiveReactions.count >= maxReactionCount { - return .set(Set(effectiveReactions.map(\.value))) + return (.set(Set(effectiveReactions.map(\.value))), areStarsEnabled) } switch reactionSettings { case .unknown: if case let .channel(channel) = peer, case .broadcast = channel.info { if let availableReactions = availableReactions { - return .set(Set(availableReactions.reactions.map(\.value))) + return (.set(Set(availableReactions.reactions.map(\.value))), areStarsEnabled) } else { - return .set(Set()) + return (.set(Set()), areStarsEnabled) } } - return .all + return (.all, areStarsEnabled) case let .known(value): switch value.allowedReactions { case .all: if case let .channel(channel) = peer, case .broadcast = channel.info { if let availableReactions = availableReactions { - return .set(Set(availableReactions.reactions.map(\.value))) + return (.set(Set(availableReactions.reactions.map(\.value))), areStarsEnabled) } else { - return .set(Set()) + return (.set(Set()), areStarsEnabled) } } - return .all + return (.all, areStarsEnabled) case let .limited(reactions): - return .set(Set(reactions)) + return (.set(Set(reactions)), areStarsEnabled) case .empty: - return .set(Set()) + return (.set(Set()), areStarsEnabled) } } } @@ -160,6 +166,8 @@ public func tagMessageReactions(context: AccountContext, subPeerId: EnginePeer.I largeApplicationAnimation: nil, isCustom: true )) + case .stars: + continue } } @@ -212,6 +220,33 @@ public func tagMessageReactions(context: AccountContext, subPeerId: EnginePeer.I largeApplicationAnimation: nil, isCustom: true )) + case .stars: + if let reaction = availableReactions?.reactions.first(where: { $0.value == .stars }) { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + + if existingIds.contains(reaction.value) { + continue + } + existingIds.insert(reaction.value) + + result.append(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + )) + } else { + continue + } } } } @@ -250,41 +285,28 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe } } - let allowedReactionsWithFiles: Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile])?, NoError> = peerMessageAllowedReactions(context: context, message: message) - |> mapToSignal { allowedReactions -> Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile])?, NoError> in + let allowedReactionsWithFiles: Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> = peerMessageAllowedReactions(context: context, message: message) + |> mapToSignal { allowedReactions, areStarsEnabled -> Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> in guard let allowedReactions = allowedReactions else { return .single(nil) } if case let .set(reactions) = allowedReactions { - #if DEBUG - var reactions = reactions - if context.sharedContext.applicationBindings.appBuildType == .internal { - reactions.insert(.custom(MessageReaction.starsReactionId)) - } - #endif - return context.engine.stickers.resolveInlineStickers(fileIds: reactions.compactMap { item -> Int64? in switch item { case .builtin: return nil case let .custom(fileId): return fileId + case .stars: + return nil } }) - |> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in - return (.set(reactions), files) + |> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool) in + return (.set(reactions), files, areStarsEnabled) } } else { - #if DEBUG - if context.sharedContext.applicationBindings.appBuildType == .internal { - return context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) - |> map { files -> (reactions: AllowedReactions, files: [Int64: TelegramMediaFile]) in - return (allowedReactions, files) - } - } - #endif - return .single((allowedReactions, [:])) + return .single((allowedReactions, [:], areStarsEnabled)) } } @@ -302,25 +324,6 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe var result: [ReactionItem] = [] var existingIds = Set() - #if DEBUG - if context.sharedContext.applicationBindings.appBuildType == .internal { - if let file = allowedReactionsAndFiles.files[MessageReaction.starsReactionId] { - existingIds.insert(.custom(MessageReaction.starsReactionId)) - - result.append(ReactionItem( - reaction: ReactionItem.Reaction(rawValue: .custom(file.fileId.id)), - appearAnimation: file, - stillAnimation: file, - listAnimation: file, - largeListAnimation: file, - applicationAnimation: nil, - largeApplicationAnimation: nil, - isCustom: true - )) - } - } - #endif - for topReaction in topReactions { switch topReaction.content { case let .builtin(value): @@ -384,6 +387,8 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe largeApplicationAnimation: nil, isCustom: true )) + case .stars: + break } } @@ -447,6 +452,28 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe isCustom: true )) } + case .stars: + break + } + } + } + + if allowedReactionsAndFiles.areStarsEnabled { + result.removeAll(where: { $0.reaction.rawValue == .stars }) + if let reaction = availableReactions.reactions.first(where: { $0.value == .stars }) { + if let centerAnimation = reaction.centerAnimation, let aroundAnimation = reaction.aroundAnimation { + existingIds.insert(reaction.value) + + result.insert(ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + ), at: 0) } } } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index e0331cb2db3..cc47b94b4ef 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -276,6 +276,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let scrollToMessageId: (MessageIndex) -> Void public let navigateToStory: (Message, StoryId) -> Void public let attemptedNavigationToPrivateQuote: (Peer?) -> Void + public let forceUpdateWarpContents: () -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] @@ -407,6 +408,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol scrollToMessageId: @escaping (MessageIndex) -> Void, navigateToStory: @escaping (Message, StoryId) -> Void, attemptedNavigationToPrivateQuote: @escaping (Peer?) -> Void, + forceUpdateWarpContents: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, @@ -519,6 +521,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.scrollToMessageId = scrollToMessageId self.navigateToStory = navigateToStory self.attemptedNavigationToPrivateQuote = attemptedNavigationToPrivateQuote + self.forceUpdateWarpContents = forceUpdateWarpContents self.automaticMediaDownloadSettings = automaticMediaDownloadSettings diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 6442a231f7d..4d91e53e12d 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -905,7 +905,7 @@ private let starImage: UIImage? = { context.clear(CGRect(origin: .zero, size: size)) if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage { - context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 1.0, dy: 1.0), byTiling: false) } })?.withRenderingMode(.alwaysTemplate) }() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift index 78240287326..e20af0a7ee2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiKeyboardItemLayer.swift @@ -336,7 +336,8 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { func update( content: EmojiPagerContentComponent.ItemContent, - theme: PresentationTheme + theme: PresentationTheme, + strings: PresentationStrings ) { var themeUpdated = false if self.theme !== theme { @@ -376,14 +377,38 @@ public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget { UIGraphicsPushContext(context) context.setFillColor(color.withMultipliedAlpha(0.2).cgColor) - context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) + + context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 21.0).cgPath) + context.fillPath() context.setFillColor(color.cgColor) - let plusSize = CGSize(width: 4.5, height: 31.5) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath) - context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath) + let plusSize = CGSize(width: 3.5, height: 28.0) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width).offsetBy(dx: 0.0, dy: -17.0), cornerRadius: plusSize.width / 2.0).cgPath) context.fillPath() + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + let string = strings.Stickers_CreateSticker + var lineOriginY = size.height / 2.0 - 18.0 + let components = string.components(separatedBy: "\n") + for component in components { + context.saveGState() + let attributedString = NSAttributedString(string: component, attributes: [NSAttributedString.Key.font: Font.medium(17.0), NSAttributedString.Key.foregroundColor: color]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOrigin = CGPoint(x: floorToScreenPixels((size.width - lineBounds.size.width) / 2.0), y: lineOriginY) + context.textPosition = lineOrigin + CTLineDraw(line, context) + + lineOriginY -= lineBounds.height + 6.0 + context.restoreGState() + } + UIGraphicsPopContext() }) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index db64a3829f4..4a780344e55 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -3510,7 +3510,7 @@ public final class EmojiPagerContentComponent: Component { } if case .icon = item.content { - itemLayer.update(content: item.content, theme: keyboardChildEnvironment.theme) + itemLayer.update(content: item.content, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings) } itemLayer.update( diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index ea06826e997..a3a42cc62c2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -793,6 +793,10 @@ public extension EmojiPagerContentComponent { } case let .custom(file): topReactionItems.append(EmojiComponentReactionItem(reaction: .custom(file.fileId.id), file: file)) + case .stars: + if let reaction = availableReactions?.reactions.first(where: { $0.value == .stars }) { + topReactionItems.append(EmojiComponentReactionItem(reaction: .stars, file: reaction.selectAnimation)) + } } } } @@ -962,6 +966,22 @@ public extension EmojiPagerContentComponent { } else { icon = .none } + case .stars: + if existingIds.contains(.stars) { + continue + } + existingIds.insert(.stars) + if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .stars }) { + if let centerAnimation = availableReaction.centerAnimation { + animationFile = centerAnimation + } else { + continue + } + } else { + continue + } + + icon = .none } var tintMode: Item.TintMode = .none @@ -1014,6 +1034,12 @@ public extension EmojiPagerContentComponent { } case let .custom(file): topReactionItems.append(EmojiComponentReactionItem(reaction: .custom(file.fileId.id), file: file)) + case .stars: + if let reaction = availableReactions?.reactions.first(where: { $0.value == .stars }) { + topReactionItems.append(EmojiComponentReactionItem(reaction: .stars, file: reaction.selectAnimation)) + } else { + continue + } } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift index 5eb70b2d572..f644aec5249 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchHeaderView.swift @@ -217,7 +217,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if let textField = self.textField, let text = textField.text, text.isEmpty { if self.bounds.contains(point), let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View { let leftTextPosition = placeholderContentView.leftTextPosition() - if point.x >= 0.0 && point.x <= placeholderContentView.frame.minX + leftTextPosition { + if point.x >= placeholderContentView.frame.minX + leftTextPosition { if let result = placeholderContentView.hitTest(self.convert(point, to: placeholderContentView), with: event) { return result } @@ -278,9 +278,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { textField.resignFirstResponder() textField.removeFromSuperview() } - - /*self.tintTextView.view?.isHidden = false - self.textView.view?.isHidden = false*/ } @objc private func clearPressed() { @@ -519,8 +516,20 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { if let term { self.update(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) + let textField = self.textField + self.textField = nil + + self.clearIconView.isHidden = true + self.clearIconTintView.isHidden = true + self.clearIconButton.isHidden = true + self.updateQuery(.category(value: term)) self.activated(false) + + if let textField { + textField.resignFirstResponder() + textField.removeFromSuperview() + } } else { self.deactivated(self.textField?.isFirstResponder ?? false) self.updateQuery(nil) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index 3c53dd5497e..8e9935d8be4 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -267,6 +267,8 @@ final class EmojiSearchSearchBarComponent: Component { private var highlightedItem: AnyHashable? private var selectedItem: AnyHashable? + private var disableInteraction: Bool = false + private lazy var hapticFeedback: HapticFeedback = { return HapticFeedback() }() @@ -421,10 +423,16 @@ final class EmojiSearchSearchBarComponent: Component { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - guard let component = self.component else { + if self.disableInteraction { + for (_, itemView) in self.visibleItemViews { + if let itemComponentView = itemView.view.view { + if itemComponentView.bounds.contains(self.convert(point, to: itemComponentView)) { + return self + } + } + } return nil } - let _ = component return super.hitTest(point, with: event) } @@ -714,10 +722,17 @@ final class EmojiSearchSearchBarComponent: Component { switch component.textInputState { case let .active(hasText): - self.isUserInteractionEnabled = false + if hasText { + self.disableInteraction = false + self.isUserInteractionEnabled = false + } else { + self.disableInteraction = true + self.isUserInteractionEnabled = true + } self.textView.view?.isHidden = hasText self.tintTextView.view?.isHidden = hasText case .inactive: + self.disableInteraction = false self.isUserInteractionEnabled = true self.textView.view?.isHidden = false self.tintTextView.view?.isHidden = false diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index ff60dcfd7d1..0adc9b1edd9 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -8220,6 +8220,8 @@ private func allowedStoryReactions(context: AccountContext) -> Signal<[ReactionI largeApplicationAnimation: nil, isCustom: true )) + case .stars: + break } } diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift index 472def405ae..6cf94f4b03d 100644 --- a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift @@ -332,7 +332,7 @@ private func notificationsPeerCategoryEntries(peerId: EnginePeer.Id, notificatio } } existingThreadIds.insert(value.threadId) - entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) + entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, .channel(TelegramChannel(id: peerId, accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [.isForum], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)), value.threadId, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) index += 1 } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 28d1151a160..b347a46bfda 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -142,6 +142,9 @@ final class PeerAllowedReactionsScreenComponent: Component { if !self.isEnabled { enabledReactions.removeAll() } + + enabledReactions.removeAll(where: { $0.reaction == .stars }) + guard let availableReactions = self.availableReactions else { return true } @@ -157,7 +160,7 @@ final class PeerAllowedReactionsScreenComponent: Component { allowedReactions = .empty } - let reactionSettings = PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: self.allowedReactionCount >= 11 ? nil : Int32(self.allowedReactionCount)) + let reactionSettings = PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: self.allowedReactionCount >= 11 ? nil : Int32(self.allowedReactionCount), starsAllowed: self.areStarsReactionsEnabled) if self.appliedReactionSettings != reactionSettings { if case .empty = allowedReactions { @@ -209,6 +212,8 @@ final class PeerAllowedReactionsScreenComponent: Component { guard var enabledReactions = self.enabledReactions else { return } + enabledReactions.removeAll(where: { $0.reaction == .stars }) + if !self.isEnabled { enabledReactions.removeAll() } @@ -223,6 +228,8 @@ final class PeerAllowedReactionsScreenComponent: Component { return true case .builtin: return false + case .stars: + return false } }) @@ -248,7 +255,7 @@ final class PeerAllowedReactionsScreenComponent: Component { } else { allowedReactions = .empty } - let reactionSettings = PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: self.allowedReactionCount == 11 ? nil : Int32(self.allowedReactionCount)) + let reactionSettings = PeerReactionSettings(allowedReactions: allowedReactions, maxReactionCount: self.allowedReactionCount == 11 ? nil : Int32(self.allowedReactionCount), starsAllowed: self.areStarsReactionsEnabled) let applyDisposable = (component.context.engine.peers.updatePeerReactionSettings(peerId: component.peerId, reactionSettings: reactionSettings) |> deliverOnMainQueue).start(error: { [weak self] error in @@ -355,14 +362,28 @@ final class PeerAllowedReactionsScreenComponent: Component { if let current = self.enabledReactions { enabledReactions = current } else { - enabledReactions = component.initialContent.enabledReactions + if let value = component.initialContent.reactionSettings?.starsAllowed { + self.areStarsReactionsEnabled = value + } else { + self.areStarsReactionsEnabled = component.initialContent.isStarReactionAvailable + } + + var enabledReactionsValue = component.initialContent.enabledReactions + if self.areStarsReactionsEnabled { + if let item = component.initialContent.availableReactions?.reactions.first(where: { $0.value == .stars }) { + enabledReactionsValue.insert(EmojiComponentReactionItem(reaction: item.value, file: item.selectAnimation), at: 0) + } + } + + enabledReactions = enabledReactionsValue self.enabledReactions = enabledReactions self.availableReactions = component.initialContent.availableReactions self.isEnabled = component.initialContent.isEnabled self.appliedReactionSettings = component.initialContent.reactionSettings.flatMap { reactionSettings in return PeerReactionSettings( allowedReactions: reactionSettings.allowedReactions, - maxReactionCount: reactionSettings.maxReactionCount == 11 ? nil : reactionSettings.maxReactionCount + maxReactionCount: reactionSettings.maxReactionCount == 11 ? nil : reactionSettings.maxReactionCount, + starsAllowed: reactionSettings.starsAllowed ) } self.allowedReactionCount = (component.initialContent.reactionSettings?.maxReactionCount).flatMap(Int.init) ?? 11 @@ -443,6 +464,8 @@ final class PeerAllowedReactionsScreenComponent: Component { return true case .builtin: return false + case .stars: + return false } }) @@ -568,6 +591,11 @@ final class PeerAllowedReactionsScreenComponent: Component { enabledReactions.append(EmojiComponentReactionItem(reaction: reactionItem.value, file: reactionItem.selectAnimation)) } } + if self.areStarsReactionsEnabled { + if let item = component.initialContent.availableReactions?.reactions.first(where: { $0.value == .stars }) { + enabledReactions.insert(EmojiComponentReactionItem(reaction: item.value, file: item.selectAnimation), at: 0) + } + } self.enabledReactions = enabledReactions self.caretPosition = enabledReactions.count } @@ -868,7 +896,7 @@ final class PeerAllowedReactionsScreenComponent: Component { } contentHeight += reactionCountSectionSize.height - if !"".isEmpty { + if component.initialContent.isStarReactionAvailable { contentHeight += 32.0 let paidReactionsSection: ComponentView @@ -879,8 +907,7 @@ final class PeerAllowedReactionsScreenComponent: Component { self.paidReactionsSection = paidReactionsSection } - //TODO:localize - let parsedString = parseMarkdownIntoAttributedString("Switch this on to let your subscribers set paid reactions with Telegram Stars, which you will be able to withdraw later as TON. [Learn More >](https://telegram.org/privacy)", attributes: MarkdownAttributes( + let parsedString = parseMarkdownIntoAttributedString(environment.strings.PeerInfo_AllowedReactions_StarReactionsFooter, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), @@ -897,7 +924,6 @@ final class PeerAllowedReactionsScreenComponent: Component { paidReactionsFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: paidReactionsFooterText.string)) } - //TODO:localize let paidReactionsSectionSize = paidReactionsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( @@ -925,13 +951,36 @@ final class PeerAllowedReactionsScreenComponent: Component { items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListSwitchItemComponent( theme: environment.theme, - title: "Enable Paid Reactions", - value: areStarsReactionsEnabled, + title: environment.strings.PeerInfo_AllowedReactions_StarReactions, + value: self.areStarsReactionsEnabled, valueUpdated: { [weak self] value in - guard let self else { + guard let self, let component = self.component else { return } self.areStarsReactionsEnabled = value + + var enabledReactions = self.enabledReactions ?? [] + if self.areStarsReactionsEnabled { + if let item = component.initialContent.availableReactions?.reactions.first(where: { $0.value == .stars }) { + enabledReactions.insert(EmojiComponentReactionItem(reaction: item.value, file: item.selectAnimation), at: 0) + if let caretPosition = self.caretPosition { + self.caretPosition = min(enabledReactions.count, caretPosition + 1) + } + } + } else { + if let index = enabledReactions.firstIndex(where: { $0.reaction == .stars }) { + enabledReactions.remove(at: index) + if let caretPosition = self.caretPosition, caretPosition > index { + self.caretPosition = max(0, caretPosition - 1) + } + } + } + + self.enabledReactions = enabledReactions + + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.25)) + } } ))) ] @@ -1049,6 +1098,8 @@ final class PeerAllowedReactionsScreenComponent: Component { return true case .builtin: return false + case .stars: + return false } }).count : 0 @@ -1126,6 +1177,11 @@ final class PeerAllowedReactionsScreenComponent: Component { self.recenterOnCaret = true } self.enabledReactions = enabledReactions + + if !enabledReactions.contains(where: { $0.reaction == .stars }) { + self.areStarsReactionsEnabled = false + } + if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.25)) } @@ -1231,17 +1287,20 @@ public class PeerAllowedReactionsScreen: ViewControllerComponentContainer { public let enabledReactions: [EmojiComponentReactionItem] public let availableReactions: AvailableReactions? public let reactionSettings: PeerReactionSettings? + public let isStarReactionAvailable: Bool init( isEnabled: Bool, enabledReactions: [EmojiComponentReactionItem], availableReactions: AvailableReactions?, - reactionSettings: PeerReactionSettings? + reactionSettings: PeerReactionSettings?, + isStarReactionAvailable: Bool ) { self.isEnabled = isEnabled self.enabledReactions = enabledReactions self.availableReactions = availableReactions self.reactionSettings = reactionSettings + self.isStarReactionAvailable = isStarReactionAvailable } public static func ==(lhs: Content, rhs: Content) -> Bool { @@ -1260,6 +1319,9 @@ public class PeerAllowedReactionsScreen: ViewControllerComponentContainer { if lhs.reactionSettings != rhs.reactionSettings { return false } + if lhs.isStarReactionAvailable != rhs.isStarReactionAvailable { + return false + } return true } } @@ -1340,6 +1402,9 @@ public class PeerAllowedReactionsScreen: ViewControllerComponentContainer { case .empty: isEnabled = false } + if let starsAllowed = reactionSettings.starsAllowed, starsAllowed { + isEnabled = true + } } var missingReactionFiles: [Int64] = [] @@ -1370,7 +1435,7 @@ public class PeerAllowedReactionsScreen: ViewControllerComponentContainer { } } - return Content(isEnabled: isEnabled, enabledReactions: result, availableReactions: availableReactions, reactionSettings: reactionSettings) + return Content(isEnabled: isEnabled, enabledReactions: result, availableReactions: availableReactions, reactionSettings: reactionSettings, isStarReactionAvailable: cachedData.flags.contains(.paidMediaAllowed)) } } |> distinctUntilChanged diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index ed6532694a2..27bcb14a8a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -182,7 +182,8 @@ public final class LoadingOverlayNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -533,6 +534,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod StoryContainerScreen.openPeerStories(context: item.context, peerId: item.data.peer.id, parentController: controller, avatarNode: itemNode.avatarNode) }, + openStarsTopup: { _ in + }, dismissNotice: { _ in }, editPeer: { _ in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index ca0a0de2a5e..5a33c2f46db 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1961,7 +1961,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: switch channelMember.participant { case .creator: break - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if adminInfo.promotedBy == accountPeerId { if !channel.flags.contains(.isGigagroup) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift index 801d58db583..81845f8a850 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift @@ -55,7 +55,7 @@ enum PeerInfoMember: Equatable { switch participant.participant { case .creator: return .creator - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if adminInfo != nil { return .admin } else { @@ -75,7 +75,7 @@ enum PeerInfoMember: Equatable { switch participant.participant { case let .creator(_, _, rank): return rank - case let .member(_, _, _, _, rank): + case let .member(_, _, _, _, rank, _): return rank } case .legacyGroupMember: diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 9a58243f490..e85effb9b72 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -48,10 +48,10 @@ import TelegramNotices import SaveToCameraRoll import PeerInfoUI // MARK: Nicegram Imports +import FeatAssistant import struct FeatPremiumUI.PremiumUITgHelper import FeatWallet import NGAiChatUI -import NGAssistantUI import NGRepoUser import NGWebUtils import NGStrings @@ -588,7 +588,6 @@ private final class PeerInfoInteraction { let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void let editingOpenStars: () -> Void - let editingToggleMessageSignatures: (Bool) -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openRecentActions: () -> Void let openStats: (ChannelStatsSection) -> Void @@ -660,7 +659,6 @@ private final class PeerInfoInteraction { editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, editingOpenStars: @escaping () -> Void, - editingToggleMessageSignatures: @escaping (Bool) -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openRecentActions: @escaping () -> Void, openStats: @escaping (ChannelStatsSection) -> Void, @@ -731,7 +729,6 @@ private final class PeerInfoInteraction { self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup self.editingOpenStars = editingOpenStars - self.editingToggleMessageSignatures = editingToggleMessageSignatures self.openParticipantsSection = openParticipantsSection self.openRecentActions = openRecentActions self.openStats = openStats @@ -2175,9 +2172,17 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL case .all: label = presentationData.strings.PeerInfo_LabelAllReactions case .empty: - label = presentationData.strings.PeerInfo_ReactionsDisabled + if let starsAllowed = reactionSettings.starsAllowed, starsAllowed { + label = "1" + } else { + label = presentationData.strings.PeerInfo_ReactionsDisabled + } case let .limited(reactions): - label = "\(reactions.count)" + var countValue = reactions.count + if let starsAllowed = reactionSettings.starsAllowed, starsAllowed { + countValue += 1 + } + label = "\(countValue)" } } else { label = "" @@ -2961,9 +2966,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenStars: { [weak self] in self?.editingOpenStars() }, - editingToggleMessageSignatures: { [weak self] value in - self?.editingToggleMessageSignatures(value: value) - }, openParticipantsSection: { [weak self] section in self?.openParticipantsSection(section: section) }, @@ -3684,6 +3686,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in @@ -5283,7 +5286,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: self.chatLocation, chatFilterTag: nil, chatLocationContextHolder: self.chatLocationContextHolder, message: galleryMessage, standalone: false, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { [weak self] in self?.view.endEditing(true) - }, present: { [weak self] c, a in + }, present: { [weak self] c, a, _ in self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) }, transitionNode: { [weak self] messageId, media, _ in guard let strongSelf = self else { @@ -5323,6 +5326,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.openHashtag(hashtag, peerName: peerName) } }, openBotCommand: { _ in + }, openAd: { _ in }, addContact: { [weak self] phoneNumber in if let strongSelf = self { strongSelf.context.sharedContext.openAddContact(context: strongSelf.context, firstName: "", lastName: "", phoneNumber: phoneNumber, label: defaultContactLabel, present: { [weak self] controller, arguments in @@ -8852,10 +8856,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = self.context.engine.peers.setChannelForumMode(id: self.peerId, isForum: isEnabled).startStandalone() } } - - private func editingToggleMessageSignatures(value: Bool) { - self.toggleShouldChannelMessagesSignaturesDisposable.set(self.context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: self.peerId, enabled: value).startStrict()) - } private func openParticipantsSection(section: PeerInfoParticipantsSection) { guard let data = self.data, let peer = data.peer else { @@ -8881,7 +8881,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let peer = self.data?.peer else { return } - let controller = self.context.sharedContext.makeChatRecentActionsController(context: self.context, peer: peer, adminPeerId: nil) + let controller = self.context.sharedContext.makeChatRecentActionsController(context: self.context, peer: peer, adminPeerId: nil, starsState: self.data?.starsRevenueStatsState) self.controller?.push(controller) } @@ -9549,7 +9549,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .fallback: (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicPhotoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) case .custom: - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessPhotoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() case .suggest: @@ -9786,7 +9786,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .fallback: (strongSelf.controller?.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: nil, text: strongSelf.presentationData.strings.Privacy_ProfilePhoto_PublicVideoSuccess, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) case .custom: - strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) + strongSelf.controller?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .invitedToVoiceChat(context: strongSelf.context, peer: peer, title: nil, text: strongSelf.presentationData.strings.UserInfo_SetCustomPhoto_SuccessVideoText(peer.compactDisplayTitle).string, action: nil, duration: 5), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) let _ = (strongSelf.context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, peerId: strongSelf.peerId, fetch: peerInfoProfilePhotos(context: strongSelf.context, peerId: strongSelf.peerId)) |> ignoreValues).startStandalone() case .suggest: @@ -11274,7 +11274,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if pane.canReorder() { - items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak pane] _, a in if ignoreNextActions { @@ -11289,7 +11289,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } - items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in if ignoreNextActions { diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift index 495541776bc..9368f7daba5 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/PremiumStarComponent.swift @@ -684,3 +684,208 @@ public final class PremiumStarComponent: Component { return view.update(component: self, availableSize: availableSize, transition: transition) } } + +public final class StandalonePremiumStarComponent: Component { + let theme: PresentationTheme + let colors: [UIColor]? + + public init( + theme: PresentationTheme, + colors: [UIColor]? = nil + ) { + self.theme = theme + self.colors = colors + } + + public static func ==(lhs: StandalonePremiumStarComponent, rhs: StandalonePremiumStarComponent) -> Bool { + return lhs.theme === rhs.theme && lhs.colors == rhs.colors + } + + public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { + public final class Tag { + public init() { + } + } + + public func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private var component: StandalonePremiumStarComponent? + + private var _ready = Promise() + public var ready: Signal { + return self._ready.get() + } + + private let sceneView: SCNView + + private var timer: SwiftSignalKit.Timer? + + override init(frame: CGRect) { + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0))) + self.sceneView.backgroundColor = .clear + self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 + self.sceneView.isJitteringEnabled = true + + super.init(frame: frame) + + self.addSubview(self.sceneView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + private var didSetup = false + private func setup() { + guard !self.didSetup, let scene = loadCompressedScene(name: "star2", version: sceneVersion) else { + return + } + + self.didSetup = true + self.sceneView.scene = scene + self.sceneView.delegate = self + + if let component = self.component, let node = scene.rootNode.childNode(withName: "star", recursively: false), let colors = + component.colors { + node.geometry?.materials.first?.diffuse.contents = generateDiffuseTexture(colors: colors) + } + + for node in scene.rootNode.childNodes { + if let name = node.name, name.hasPrefix("particles") { + node.isHidden = true + } + } + + self.didSetReady = true + self._ready.set(.single(true)) + self.onReady() + } + + private var didSetReady = false + public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + if !self.didSetReady { + self.didSetReady = true + + Queue.mainQueue().justDispatch { + self._ready.set(.single(true)) + self.onReady() + } + } + } + + private func onReady() { + //self.setupScaleAnimation() + //self.setupGradientAnimation() + + self.playAppearanceAnimation(mirror: true) + } + + private func setupScaleAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + let fromScale: Float = 0.1 + let toScale: Float = 0.092 + + let animation = CABasicAnimation(keyPath: "scale") + animation.duration = 2.0 + animation.fromValue = NSValue(scnVector3: SCNVector3(x: fromScale, y: fromScale, z: fromScale)) + animation.toValue = NSValue(scnVector3: SCNVector3(x: toScale, y: toScale, z: toScale)) + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.addAnimation(animation, forKey: "scale") + } + + private func setupGradientAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else { + return + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.duration = 4.5 + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0)) + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient") + } + + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + var from = node.presentation.eulerAngles + if abs(from.y - .pi * 2.0) < 0.001 { + from.y = 0.0 + } + node.removeAnimation(forKey: "tapRotate") + + var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 + if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 { + toValue *= -1 + } + if mirror { + toValue *= -1 + } + let to = SCNVector3(x: 0.0, y: toValue, z: 0.0) + let distance = rad2deg(to.y - from.y) + + guard !distance.isZero else { + return + } + + let animation = CABasicAnimation(keyPath: "eulerAngles") + animation.fromValue = NSValue(scnVector3: from) + animation.toValue = NSValue(scnVector3: to) + animation.duration = 0.4 * UIView.animationDurationFactor() + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.completion = { [weak node] finished in + if finished { + node?.eulerAngles = SCNVector3(x: 0.0, y: 0.0, z: 0.0) + } + } + node.addAnimation(animation, forKey: "rotate") + } + + func update(component: StandalonePremiumStarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + + self.setup() + + self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0)) + if self.sceneView.superview == self { + self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 13114175dbb..6aeef906f75 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -202,6 +202,8 @@ final class GreetingMessageListItemComponent: Component { }, openStories: { _, _ in }, + openStarsTopup: { _ in + }, dismissNotice: { _ in }, editPeer: { _ in diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 61127abfb45..bf1e17537d7 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -217,6 +217,8 @@ final class QuickReplySetupScreenComponent: Component { }, openStories: { _, _ in }, + openStarsTopup: { _ in + }, dismissNotice: { _ in }, editPeer: { [weak listNode] _ in diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift index d7c97d8c048..d44e1374d5a 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift @@ -338,6 +338,15 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { } case let .custom(fileId): animationContent = .customEmoji(fileId: fileId) + case .stars: + if let availableReactions = item.availableReactions { + for reaction in availableReactions.reactions { + if reaction.value == item.reaction { + animationContent = .file(file: reaction.selectAnimation) + break + } + } + } } diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift index da5694a1f65..fa0225bc2f9 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift @@ -307,6 +307,13 @@ public func quickReactionSetupController( } case let .custom(fileId): currentSelectedFileId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + case .stars: + if let availableReactions = availableReactions { + if let reaction = availableReactions.reactions.first(where: { $0.value == settings.quickReaction }) { + currentSelectedFileId = reaction.selectAnimation.fileId + break + } + } } var selectedItems = Set() diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index 658f8e70b70..d8c56614de8 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -207,6 +207,31 @@ class ReactionChatPreviewItemNode: ListViewItemNode { strongSelf.beginReactionAnimation(reactionItem: reactionItem) } }) + case .stars: + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + + if reaction.value == updatedReaction { + let reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + ) + self.beginReactionAnimation(reactionItem: reactionItem) + + break + } + } } } } @@ -289,7 +314,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: reaction, isLarge: false, isUnseen: false, isMy: true, peerId: accountPeer.id, timestamp: nil)) peers[accountPeer.id] = accountPeer } - attributes.append(ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers)) + attributes.append(ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers, topPeers: [])) } let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: true, isStandalone: false) diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 0ce8b9d7982..819d491cbdc 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -870,6 +870,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in + }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -957,7 +958,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) - let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil)) + let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil)) let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil)) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 8b9f6ccbe8b..9047c66cc4f 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -539,7 +539,7 @@ public extension ShareWithPeersScreen { continue } - if case let .member(_, date, _, _, _) = participant.participant { + if case let .member(_, date, _, _, _, _) = participant.participant { invitedAt[participant.peer.id] = date } else { continue @@ -557,7 +557,7 @@ public extension ShareWithPeersScreen { continue } - if case let .member(_, date, _, _, _) = participant.participant { + if case let .member(_, date, _, _, _, _) = participant.participant { invitedAt[participant.peer.id] = date } else { continue diff --git a/submodules/TelegramUI/Components/SpaceWarpView/BUILD b/submodules/TelegramUI/Components/SpaceWarpView/BUILD index bfa7fe65eb3..2118beea582 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/BUILD +++ b/submodules/TelegramUI/Components/SpaceWarpView/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/SpaceWarpView/STCMeshView", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift new file mode 100644 index 00000000000..874704418a7 --- /dev/null +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/MeshLayer.swift @@ -0,0 +1,7 @@ +import Foundation +import UIKit +import Display + +final class MeshLayer: CALayer { + +} diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index 4d77d7cc97b..e9f6392579f 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -4,6 +4,7 @@ import Display import AsyncDisplayKit import ComponentFlow import STCMeshView +import UIKitRuntimeUtils private final class FPSView: UIView { private var lastTimestamp: Double? @@ -124,7 +125,7 @@ private func rippleOffset( } if distance <= 60.0 { - rippleAmount = 0.4 * rippleAmount + rippleAmount = 0.3 * rippleAmount } // A vector of length `amplitude` that points away from position. @@ -250,6 +251,126 @@ public protocol SpaceWarpNode: ASDisplayNode { func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) } +private final class MaskGridLayer: SimpleLayer { + private var itemLayers: [SimpleLayer] = [] + + private var resolution: (x: Int, y: Int)? + + func updateGrid(size: CGSize, resolutionX: Int, resolutionY: Int, cornerRadius: CGFloat) { + if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY { + return + } + self.resolution = (resolutionX, resolutionY) + + for itemLayer in self.itemLayers { + itemLayer.removeFromSuperlayer() + } + self.itemLayers.removeAll() + + let itemSize = CGSize(width: size.width / CGFloat(resolutionX), height: size.height / CGFloat(resolutionY)) + + let topLeftCorner = CGRect(origin: CGPoint(), size: CGSize(width: cornerRadius, height: cornerRadius)) + let topRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: 0.0), size: CGSize(width: cornerRadius, height: cornerRadius)) + let bottomLeftCorner = CGRect(origin: CGPoint(x: 0.0, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius)) + let bottomRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius)) + + var cornersImage: UIImage? + if cornerRadius > 0.0 { + cornersImage = generateImage(CGSize(width: cornerRadius * 2.0 + 200.0, height: cornerRadius * 2.0 + 200.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.black.cgColor) + context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath) + context.fillPath() + }) + } + + for y in 0 ..< resolutionY { + for x in 0 ..< resolutionX { + let itemLayer = SimpleLayer() + itemLayer.backgroundColor = UIColor.black.cgColor + itemLayer.isOpaque = true + itemLayer.opacity = 1.0 + itemLayer.anchorPoint = CGPoint() + self.addSublayer(itemLayer) + self.itemLayers.append(itemLayer) + + if cornerRadius > 0.0, let cornersImage { + let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY)) + let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize) + if sourceRect.intersects(topLeftCorner) || sourceRect.intersects(topRightCorner) || sourceRect.intersects(bottomLeftCorner) || sourceRect.intersects(bottomRightCorner) { + var clippedCornersRect = sourceRect + if clippedCornersRect.maxX > cornersImage.size.width { + clippedCornersRect.origin.x -= size.width - cornersImage.size.width + } + if clippedCornersRect.maxY > cornersImage.size.height { + clippedCornersRect.origin.y -= size.height - cornersImage.size.height + } + + itemLayer.contents = cornersImage.cgImage + itemLayer.contentsRect = CGRect(origin: CGPoint(x: clippedCornersRect.minX / cornersImage.size.width, y: clippedCornersRect.minY / cornersImage.size.height), size: CGSize(width: clippedCornersRect.width / cornersImage.size.width, height: clippedCornersRect.height / cornersImage.size.height)) + itemLayer.backgroundColor = nil + itemLayer.isOpaque = false + } + } + } + } + } + + func update(positions: [CGPoint], bounds: [CGRect], transforms: [CATransform3D]) { + for i in 0 ..< self.itemLayers.count { + if i < positions.count && i < bounds.count && i < transforms.count { + let itemLayer = self.itemLayers[i] + itemLayer.position = positions[i] + itemLayer.bounds = bounds[i] + itemLayer.transform = transforms[i] + } + } + } +} + +private final class PrivateContentLayerRestoreContext { + final class Reference { + weak var layer: CALayer? + + init(layer: CALayer) { + self.layer = layer + } + } + + private static func collectPrivateContentLayers(layer: CALayer, into references: inout [Reference]) { + if getLayerDisableScreenshots(layer) { + references.append(Reference(layer: layer)) + } + if let sublayers = layer.sublayers { + for sublayer in sublayers { + collectPrivateContentLayers(layer: sublayer, into: &references) + } + } + } + + private let references: [Reference] + + init(rootLayer: CALayer) { + var references: [Reference] = [] + PrivateContentLayerRestoreContext.collectPrivateContentLayers(layer: rootLayer, into: &references) + self.references = references + + for reference in self.references { + if let layer = reference.layer { + setLayerDisableScreenshots(layer, false) + } + } + } + + func restore() { + for reference in self.references { + if let layer = reference.layer { + setLayerDisableScreenshots(layer, true) + } + } + } +} + open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { private final class Shockwave { let startPoint: CGPoint @@ -269,9 +390,10 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { private var currentCloneView: UIView? private var meshView: STCMeshView? - private var gradientLayer: SimpleGradientLayer? + private var privateContentRestoreContext: PrivateContentLayerRestoreContext? - private var debugLayers: [SimpleLayer] = [] + private var gradientLayer: SimpleGradientLayer? + private var gradientMaskLayer: MaskGridLayer? #if DEBUG private var fpsView: FPSView? @@ -306,17 +428,41 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { #endif } + public static func supportsHierarchy(layer: CALayer) -> Bool { + if getLayerDisableScreenshots(layer) { + return false + } + if let sublayers = layer.sublayers { + for sublayer in sublayers { + if !supportsHierarchy(layer: sublayer) { + return false + } + } + } + return true + } + public func triggerRipple(at point: CGPoint) { + if !SpaceWarpNodeImpl.supportsHierarchy(layer: self.contentNodeSource.view.layer) { + return + } + self.shockwaves.append(Shockwave(startPoint: point)) if self.shockwaves.count > 8 { self.shockwaves.removeFirst() } if self.link == nil { - self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + var previousTimestamp = CACurrentMediaTime() + self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in guard let self else { return } + + let timestamp = CACurrentMediaTime() + let deltaTime = max(0.0, min(10.0 / 60.0, timestamp - previousTimestamp)) + previousTimestamp = timestamp + for shockwave in self.shockwaves { shockwave.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor())) } @@ -338,24 +484,12 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { self.meshView = nil meshView.removeFromSuperview() } - for debugLayer in self.debugLayers { - debugLayer.removeFromSuperlayer() - } - self.debugLayers.removeAll() let meshView = STCMeshView(frame: CGRect()) self.meshView = meshView self.view.insertSubview(meshView, aboveSubview: self.backgroundView) meshView.instanceCount = resolutionX * resolutionY - - /*for _ in 0 ..< resolutionX * resolutionY { - let debugLayer = SimpleLayer() - debugLayer.backgroundColor = UIColor.red.cgColor - debugLayer.opacity = 1.0 - self.layer.addSublayer(debugLayer) - self.debugLayers.append(debugLayer) - }*/ } public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { @@ -368,7 +502,7 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - let params = RippleParams(amplitude: 20.0, frequency: 15.0, decay: 8.0, speed: 1400.0) + let params = RippleParams(amplitude: 10.0, frequency: 15.0, decay: 5.5, speed: 1400.0) if let currentCloneView = self.currentCloneView { currentCloneView.removeFromSuperview() @@ -396,11 +530,6 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { meshView.removeFromSuperview() } - for debugLayer in self.debugLayers { - debugLayer.removeFromSuperlayer() - } - self.debugLayers.removeAll() - self.resolution = nil self.backgroundView.isHidden = true self.contentNodeSource.clipsToBounds = false @@ -410,41 +539,36 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { self.gradientLayer = nil gradientLayer.removeFromSuperlayer() } + if let gradientMaskLayer = self.gradientMaskLayer { + self.gradientMaskLayer = nil + gradientMaskLayer.removeFromSuperlayer() + } + + if let privateContentRestoreContext = self.privateContentRestoreContext { + self.privateContentRestoreContext = nil + privateContentRestoreContext.restore() + } return } + if self.privateContentRestoreContext == nil { + self.privateContentRestoreContext = PrivateContentLayerRestoreContext(rootLayer: self.contentNodeSource.view.layer) + } + self.backgroundView.isHidden = false self.contentNodeSource.clipsToBounds = true self.contentNodeSource.layer.cornerRadius = cornerRadius - /*let gradientLayer: SimpleGradientLayer - if let current = self.gradientLayer { - gradientLayer = current - } else { - gradientLayer = SimpleGradientLayer() - self.gradientLayer = gradientLayer - self.layer.addSublayer(gradientLayer) - - gradientLayer.type = .radial - gradientLayer.colors = [UIColor.clear.cgColor, UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor, UIColor.clear.cgColor] - } - gradientLayer.frame = CGRect(origin: CGPoint(), size: size) - - gradientLayer.startPoint = CGPoint(x: startPoint.x / size.width, y: startPoint.x / size.height) - let radius = CGSize(width: maxEdge, height: maxEdge) - let endEndPoint = CGPoint(x: (gradientLayer.startPoint.x + radius.width) * 1.0, y: (gradientLayer.startPoint.y + radius.height) * 1.0) - gradientLayer.endPoint = endEndPoint - - let progress = max(0.0, min(1.0, self.timeValue / maxDelay))*/ - #if DEBUG if let fpsView = self.fpsView { fpsView.update() } #endif - self.updateGrid(resolutionX: max(2, Int(size.width / 40.0)), resolutionY: max(2, Int(size.height / 40.0))) + let resolutionX = max(2, Int(size.width / 40.0)) + let resolutionY = max(2, Int(size.height / 40.0)) + self.updateGrid(resolutionX: resolutionX, resolutionY: resolutionY) guard let resolution = self.resolution, let meshView = self.meshView else { return } @@ -456,6 +580,60 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { meshView.frame = CGRect(origin: CGPoint(), size: size) + if let shockwave = self.shockwaves.first { + let gradientMaskLayer: MaskGridLayer + if let current = self.gradientMaskLayer { + gradientMaskLayer = current + } else { + gradientMaskLayer = MaskGridLayer() + self.gradientMaskLayer = gradientMaskLayer + } + + let gradientLayer: SimpleGradientLayer + if let current = self.gradientLayer { + gradientLayer = current + } else { + gradientLayer = SimpleGradientLayer() + self.gradientLayer = gradientLayer + self.layer.addSublayer(gradientLayer) + + gradientLayer.type = .radial + gradientLayer.colors = [UIColor(white: 1.0, alpha: 0.0).cgColor, UIColor(white: 1.0, alpha: 0.0).cgColor, UIColor(white: 1.0, alpha: 0.2).cgColor, UIColor(white: 1.0, alpha: 0.0).cgColor] + + gradientLayer.mask = gradientMaskLayer + } + + gradientLayer.frame = CGRect(origin: CGPoint(), size: size) + gradientMaskLayer.frame = CGRect(origin: CGPoint(), size: size) + + gradientLayer.startPoint = CGPoint(x: shockwave.startPoint.x / size.width, y: shockwave.startPoint.y / size.height) + + let distance = shockwave.timeValue * params.speed + let progress = max(0.0, distance / min(size.width, size.height)) + + let radius = CGSize(width: 1.0 * progress, height: (size.width / size.height) * progress) + let endEndPoint = CGPoint(x: (gradientLayer.startPoint.x + radius.width), y: (gradientLayer.startPoint.y + radius.height)) + gradientLayer.endPoint = endEndPoint + + let maxWavefrontNorm: CGFloat = 0.4 + + let normProgress = max(0.0, min(1.0, progress)) + let interpolatedNorm: CGFloat = 1.0 * (1.0 - normProgress) + maxWavefrontNorm * normProgress + let wavefrontNorm: CGFloat = max(0.01, min(0.99, interpolatedNorm)) + + gradientLayer.locations = ([0.0, 1.0 - wavefrontNorm, 1.0 - wavefrontNorm * 0.5, 1.0] as [CGFloat]).map { $0 as NSNumber } + + let alphaProgress: CGFloat = max(0.0, min(1.0, normProgress / 0.15)) + var interpolatedAlpha: CGFloat = alphaProgress + interpolatedAlpha = max(0.0, min(1.0, interpolatedAlpha)) + gradientLayer.opacity = Float(interpolatedAlpha) + } else { + if let gradientLayer = self.gradientLayer { + self.gradientLayer = nil + gradientLayer.removeFromSuperlayer() + } + } + let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y)) var instanceBounds: [CGRect] = [] @@ -520,10 +698,9 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { meshView.instanceTransforms = buffer.baseAddress! } - for i in 0 ..< self.debugLayers.count { - self.debugLayers[i].bounds = instanceBounds[i] - self.debugLayers[i].position = instancePositions[i] - self.debugLayers[i].transform = instanceTransforms[i] + if let gradientMaskLayer = self.gradientMaskLayer { + gradientMaskLayer.updateGrid(size: size, resolutionX: resolutionX, resolutionY: resolutionY, cornerRadius: cornerRadius) + gradientMaskLayer.update(positions: instancePositions, bounds: instanceBounds, transforms: instanceTransforms) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 679bb8df662..e05bba66bdc 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -309,22 +309,29 @@ public final class StarsAvatarComponent: Component { public final class StarsLabelComponent: CombinedComponent { let text: NSAttributedString + let subtext: NSAttributedString? public init( - text: NSAttributedString + text: NSAttributedString, + subtext: NSAttributedString? = nil ) { self.text = text + self.subtext = subtext } public static func ==(lhs: StarsLabelComponent, rhs: StarsLabelComponent) -> Bool { if lhs.text != rhs.text { return false } + if lhs.subtext != rhs.subtext { + return false + } return true } public static var body: Body { let text = Child(MultilineTextComponent.self) + let subLabel = Child(MultilineTextComponent.self) let icon = Child(BundleIconComponent.self) return { context in @@ -332,30 +339,59 @@ public final class StarsLabelComponent: CombinedComponent { let text = text.update( component: MultilineTextComponent(text: .plain(component.text)), - availableSize: CGSize(width: 100.0, height: 40.0), + availableSize: CGSize(width: 140.0, height: 40.0), transition: context.transition ) + + var subtext: _UpdatedChildComponent? = nil + if let sublabel = component.subtext { + subtext = subLabel.update( + component: MultilineTextComponent(text: .plain(sublabel)), + availableSize: CGSize(width: 100.0, height: 40.0), + transition: context.transition + ) + } + let iconSize = CGSize(width: 20.0, height: 20.0) let icon = icon.update( component: BundleIconComponent( - name: "Premium/Stars/StarLarge", + name: "Premium/Stars/StarMedium", tintColor: nil ), availableSize: iconSize, transition: context.transition ) - let spacing: CGFloat = 3.0 + let spacing: CGFloat = 0.0 let totalWidth = text.size.width + spacing + iconSize.width - let size = CGSize(width: totalWidth, height: iconSize.height) + var size = CGSize(width: totalWidth, height: iconSize.height) + let firstLineSize = size.height + if let subtext { + size.height += subtext.size.height + } + + let iconPosition: CGFloat + let textPosition: CGFloat + if let _ = component.subtext { + iconPosition = iconSize.width / 2.0 + textPosition = totalWidth - text.size.width / 2.0 + } else { + textPosition = text.size.width / 2.0 + iconPosition = totalWidth - iconSize.width / 2.0 + } context.add(text - .position(CGPoint(x: text.size.width / 2.0, y: size.height / 2.0)) + .position(CGPoint(x: textPosition, y: firstLineSize / 2.0)) ) context.add(icon - .position(CGPoint(x: totalWidth - iconSize.width / 2.0, y: size.height / 2.0 - UIScreenPixel)) + .position(CGPoint(x: iconPosition, y: firstLineSize / 2.0 - UIScreenPixel)) ) + if let subtext { + context.add(subtext + .position(CGPoint(x: size.width - subtext.size.width / 2.0, y: firstLineSize + subtext.size.height / 2.0)) + ) + } return size } } diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 6c1e5dc9484..72e92b950f6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -297,11 +297,16 @@ public final class StarsImageComponent: Component { } } + public enum Icon { + case star + } + public let context: AccountContext public let subject: Subject public let theme: PresentationTheme public let diameter: CGFloat public let backgroundColor: UIColor + public let icon: Icon? public let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? public init( @@ -310,6 +315,7 @@ public final class StarsImageComponent: Component { theme: PresentationTheme, diameter: CGFloat, backgroundColor: UIColor, + icon: Icon? = nil, action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil ) { self.context = context @@ -317,6 +323,7 @@ public final class StarsImageComponent: Component { self.theme = theme self.diameter = diameter self.backgroundColor = backgroundColor + self.icon = icon self.action = action } @@ -336,6 +343,9 @@ public final class StarsImageComponent: Component { if lhs.backgroundColor != rhs.backgroundColor { return false } + if lhs.icon != rhs.icon { + return false + } return true } @@ -353,6 +363,8 @@ public final class StarsImageComponent: Component { private var avatarNode: ImageNode? private var iconBackgroundView: UIImageView? private var iconView: UIImageView? + private var smallIconOutlineView: UIImageView? + private var smallIconView: UIImageView? private var dustNode: MediaDustNode? private var button: UIControl? @@ -814,6 +826,39 @@ public final class StarsImageComponent: Component { animationNode.updateLayout(size: animationFrame.size) } + if let _ = component.icon { + let smallIconView: UIImageView + let smallIconOutlineView: UIImageView + if let current = self.smallIconView, let currentOutline = self.smallIconOutlineView { + smallIconView = current + smallIconOutlineView = currentOutline + } else { + smallIconOutlineView = UIImageView() + containerNode.view.addSubview(smallIconOutlineView) + self.smallIconOutlineView = smallIconOutlineView + + smallIconView = UIImageView() + containerNode.view.addSubview(smallIconView) + self.smallIconView = smallIconView + + smallIconOutlineView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStarOutline")?.withRenderingMode(.alwaysTemplate) + smallIconView.image = UIImage(bundleImageName: "Premium/Stars/TransactionStar") + } + + smallIconOutlineView.tintColor = component.backgroundColor + + if let icon = smallIconView.image { + let smallIconFrame = CGRect(origin: CGPoint(x: imageFrame.maxX - icon.size.width, y: imageFrame.maxY - icon.size.height), size: icon.size) + smallIconView.frame = smallIconFrame + smallIconOutlineView.frame = smallIconFrame + } + } else if let smallIconView = self.smallIconView, let smallIconOutlineView = self.smallIconOutlineView { + self.smallIconView = nil + smallIconView.removeFromSuperview() + self.smallIconOutlineView = nil + smallIconOutlineView.removeFromSuperview() + } + if let _ = component.action { if self.button == nil { let button = UIControl(frame: imageFrame) diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index a5257985bd5..7554de748b6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -208,9 +208,24 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let textString: String switch context.component.purpose { - case let .generic(requiredStars): - let _ = requiredStars + case .generic: textString = strings.Stars_Purchase_GetStarsInfo + case let .topUp(_, purpose): + var text = strings.Stars_Purchase_GenericPurchasePurpose + if let purpose, !purpose.isEmpty { + switch purpose { + case "subs": + text = strings.Stars_Purchase_PurchasePurpose_subs + default: + let key = "Stars.Purchase.PurchasePurpose.\(purpose)" + if let string = strings.primaryComponent.dict[key] { + text = string + } else if let string = strings.secondaryComponent?.dict[key] { + text = string + } + } + } + textString = text case .gift: textString = strings.Stars_Purchase_GiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string case .transfer: @@ -452,7 +467,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } }, tapAction: { attributes, _ in - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Purchase_Terms_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Purchase_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: nil, dismissInput: {}) } ), environment: {}, @@ -816,12 +831,10 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { let titleText: String switch context.component.purpose { - case let .generic(requiredStars): - if let requiredStars { - titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) - } else { - titleText = strings.Stars_Purchase_GetStars - } + case .generic: + titleText = strings.Stars_Purchase_GetStars + case let .topUp(requiredStars, _): + titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) case .gift: titleText = strings.Stars_Purchase_GiftStars case let .transfer(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars): @@ -1156,7 +1169,7 @@ func generateStarsIcon(count: Int) -> UIImage { let mainImage = UIImage(bundleImageName: "Premium/Stars/StarLarge") if let cgImage = mainImage?.cgImage, let partCGImage = partImage.cgImage { - context.draw(cgImage, in: CGRect(origin: CGPoint(x: originX, y: 0.0), size: imageSize), byTiling: false) + context.draw(cgImage, in: CGRect(origin: CGPoint(x: originX, y: 0.0), size: imageSize).insetBy(dx: -1.5, dy: -1.5), byTiling: false) originX += spacing + UIScreenPixel for _ in 0 ..< count - 1 { @@ -1226,7 +1239,7 @@ private extension StarsPurchasePurpose { var requiredStars: Int64? { switch self { - case let .generic(requiredStars): + case let .topUp(requiredStars, _): return requiredStars case let .transfer(_, requiredStars): return requiredStars diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index ee53a897b4a..899149a3619 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -30,34 +30,34 @@ private final class StarsTransactionSheetContent: CombinedComponent { let context: AccountContext let subject: StarsTransactionScreen.Subject - let action: () -> Void let cancel: (Bool) -> Void let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openAppExamples: () -> Void let copyTransactionId: (String) -> Void + let updateSubscription: () -> Void init( context: AccountContext, subject: StarsTransactionScreen.Subject, - action: @escaping () -> Void, cancel: @escaping (Bool) -> Void, openPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, - copyTransactionId: @escaping (String) -> Void + copyTransactionId: @escaping (String) -> Void, + updateSubscription: @escaping () -> Void ) { self.context = context self.subject = subject - self.action = action self.cancel = cancel self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId + self.updateSubscription = updateSubscription } static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { @@ -97,6 +97,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { peerIds.append(receipt.botPaymentId) case let .gift(message): peerIds.append(message.id.peerId) + case let .subscription(subscription): + peerIds.append(subscription.peer.id) + case let .importer(_, _, importer, _): + peerIds.append(importer.peer.peerId) } self.disposable = (context.engine.data.get( @@ -139,10 +143,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { let description = Child(MultilineTextComponent.self) let table = Child(TableComponent.self) let additional = Child(BalancedTextComponent.self) + let status = Child(BalancedTextComponent.self) let button = Child(SolidRoundedButtonComponent.self) - let refundBackgound = Child(RoundedRectangle.self) - let refundText = Child(MultilineTextComponent.self) + let transactionStatusBackgound = Child(RoundedRectangle.self) + let transactionStatusText = Child(MultilineTextComponent.self) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) @@ -183,45 +188,149 @@ private final class StarsTransactionSheetContent: CombinedComponent { let titleText: String let amountText: String var descriptionText: String - let additionalText: String - let buttonText: String + let additionalText = strings.Stars_Transaction_Terms + var buttonText: String? = strings.Common_OK + var buttonIsDestructive = false + var statusText: String? + var statusIsDestructive = false let count: Int64 var countIsGeneric = false var countOnTop = false - let transactionId: String? + var transactionId: String? let date: Int32 - let via: String? - let messageId: EngineMessage.Id? - let toPeer: EnginePeer? - let transactionPeer: StarsContext.State.Transaction.Peer? - let media: [AnyMediaReference] - let photo: TelegramMediaWebFile? - let isRefund: Bool - let isGift: Bool + var additionalDate: Int32? + var via: String? + var messageId: EngineMessage.Id? + var toPeer: EnginePeer? + var transactionPeer: StarsContext.State.Transaction.Peer? + var media: [AnyMediaReference] = [] + var photo: TelegramMediaWebFile? + var transactionStatus: (String, UIColor)? = nil + var isGift = false + var isSubscription = false + var isSubscriber = false + var isSubscriptionFee = false + var isCancelled = false + var isReaction = false var delayedCloseOnOpenPeer = true switch subject { + case let .importer(peer, pricing, importer, usdRate): + let usdValue = formatTonUsdValue(pricing.amount, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat) + titleText = strings.Stars_Transaction_Subscription_Title + descriptionText = strings.Stars_Transaction_Subscription_PerMonthUsd(usdValue).string + count = pricing.amount + countOnTop = true + date = importer.date + toPeer = importer.peer.peer.flatMap(EnginePeer.init) + transactionPeer = .peer(peer) + isSubscriber = true + case let .subscription(subscription): + titleText = strings.Stars_Transaction_Subscription_Title + descriptionText = "" + count = subscription.pricing.amount + date = subscription.untilDate + if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 { + additionalDate = creationDate + } else { + additionalDate = nil + } + toPeer = subscription.peer + transactionPeer = .peer(subscription.peer) + isSubscription = true + + var hasLeft = false + var isKicked = false + if let toPeer, case let .channel(channel) = toPeer { + switch channel.participationStatus { + case .left: + hasLeft = true + case .kicked: + isKicked = true + default: + break + } + } + + if hasLeft || isKicked { + if subscription.flags.contains(.isCancelled) { + statusText = strings.Stars_Transaction_Subscription_Cancelled + statusIsDestructive = true + if date > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + buttonText = strings.Stars_Transaction_Subscription_Renew + } else { + if let _ = subscription.inviteHash, !isKicked { + buttonText = strings.Stars_Transaction_Subscription_JoinAgainChannel + } else { + buttonText = strings.Common_OK + } + } + } else { + if date < Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + statusText = strings.Stars_Transaction_Subscription_Expired(stringForMediumDate(timestamp: subscription.untilDate, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)).string + buttonText = strings.Stars_Transaction_Subscription_Renew + } else { + statusText = strings.Stars_Transaction_Subscription_LeftChannel(stringForMediumDate(timestamp: subscription.untilDate, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)).string + buttonText = strings.Stars_Transaction_Subscription_JoinChannel + } + } + isCancelled = true + } else { + if subscription.flags.contains(.isCancelled) { + statusText = strings.Stars_Transaction_Subscription_Cancelled + statusIsDestructive = true + if date > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + buttonText = strings.Stars_Transaction_Subscription_Renew + } else { + if let _ = subscription.inviteHash, !isKicked { + buttonText = strings.Stars_Transaction_Subscription_JoinAgainChannel + } else { + buttonText = strings.Common_OK + } + } + } else { + statusText = strings.Stars_Transaction_Subscription_Active(stringForMediumDate(timestamp: subscription.untilDate, strings: strings, dateTimeFormat: dateTimeFormat, withTime: false)).string + buttonText = strings.Stars_Transaction_Subscription_Cancel + buttonIsDestructive = true + } + } case let .transaction(transaction, parentPeer): - if transaction.flags.contains(.isGift) { + if let _ = transaction.subscriptionPeriod { + titleText = strings.Stars_Transaction_SubscriptionFee + descriptionText = "" + count = transaction.count + transactionId = transaction.id + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } + transactionPeer = transaction.peer + isSubscriptionFee = true + } else if transaction.flags.contains(.isGift) { titleText = strings.Stars_Gift_Received_Title descriptionText = strings.Stars_Gift_Received_Text count = transaction.count countOnTop = true transactionId = transaction.id - via = nil - messageId = nil date = transaction.date if case let .peer(peer) = transaction.peer { toPeer = peer - } else { - toPeer = nil } transactionPeer = transaction.peer - media = [] - photo = nil - isRefund = false isGift = true + } else if transaction.flags.contains(.isReaction) { + titleText = strings.Stars_Transaction_Reaction_Title + descriptionText = "" + messageId = transaction.paidMessageId + count = transaction.count + transactionId = transaction.id + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } + transactionPeer = transaction.peer + isReaction = true } else { switch transaction.peer { case let .peer(peer): @@ -230,7 +339,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { titleText = transaction.title ?? peer.compactDisplayTitle } - via = nil case .appStore: titleText = strings.Stars_Transaction_AppleTopUp_Title via = strings.Stars_Transaction_AppleTopUp_Subtitle @@ -253,7 +361,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { via = strings.Stars_Transaction_TelegramAds_Subtitle case .unsupported: titleText = strings.Stars_Transaction_Unsupported_Title - via = nil } if !transaction.media.isEmpty { var description: String = "" @@ -293,33 +400,27 @@ private final class StarsTransactionSheetContent: CombinedComponent { date = transaction.date if case let .peer(peer) = transaction.peer { toPeer = peer - } else { - toPeer = nil } transactionPeer = transaction.peer media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) } photo = transaction.photo - isGift = false - isRefund = transaction.flags.contains(.isRefund) + + if transaction.flags.contains(.isRefund) { + transactionStatus = (strings.Stars_Transaction_Refund, theme.list.itemDisclosureActions.constructive.fillColor) + } else if transaction.flags.contains(.isPending) { + transactionStatus = (strings.Monetization_Transaction_Pending, theme.list.itemDisclosureActions.warning.fillColor) + } } case let .receipt(receipt): titleText = receipt.invoiceMedia.title descriptionText = receipt.invoiceMedia.description count = (receipt.invoice.prices.first?.amount ?? receipt.invoiceMedia.totalAmount) * -1 - via = nil - messageId = nil transactionId = receipt.transactionId date = receipt.date if let peer = state.peerMap[receipt.botPaymentId] { toPeer = peer - } else { - toPeer = nil } - transactionPeer = nil - media = [] photo = receipt.invoiceMedia.photo - isRefund = false - isGift = false delayedCloseOnOpenPeer = false case let .gift(message): let incoming = message.flags.contains(.Incoming) @@ -336,18 +437,12 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { fatalError() } - via = nil - messageId = nil date = message.timestamp if message.id.peerId.id._internalGetInt64Value() == 777000 { toPeer = nil } else { toPeer = state.peerMap[message.id.peerId] } - transactionPeer = nil - media = [] - photo = nil - isRefund = false isGift = true delayedCloseOnOpenPeer = false } @@ -367,7 +462,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { let formattedAmount = presentationStringsFormattedNumber(abs(Int32(count)), dateTimeFormat.groupingSeparator) let countColor: UIColor - if countIsGeneric { + if isSubscription || isSubscriber { + amountText = strings.Stars_Transaction_Subscription_PerMonth(formattedAmount).string + countColor = theme.list.itemSecondaryTextColor + } else if countIsGeneric { amountText = "\(formattedAmount)" countColor = theme.list.itemPrimaryTextColor } else if count < 0 { @@ -377,8 +475,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { amountText = "+ \(formattedAmount)" countColor = theme.list.itemDisclosureActions.constructive.fillColor } - additionalText = strings.Stars_Transaction_Terms - buttonText = strings.Common_OK let title = title.update( component: MultilineTextComponent( @@ -396,6 +492,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject + let imageIcon: StarsImageComponent.Icon? if isGift { imageSubject = .gift(count) } else if !media.isEmpty { @@ -409,6 +506,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { imageSubject = .none } + if isSubscription || isSubscriber || isSubscriptionFee { + imageIcon = .star + } else { + imageIcon = nil + } let star = star.update( component: StarsImageComponent( context: component.context, @@ -416,6 +518,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { theme: theme, diameter: 90.0, backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, + icon: imageIcon, action: !media.isEmpty ? { transitionNode, addToTransitionSurface in component.openMedia(media.map { $0.media }, transitionNode, addToTransitionSurface) } : nil @@ -424,7 +527,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: .immediate ) - let amountAttributedText = NSMutableAttributedString(string: amountText, font: Font.semibold(17.0), textColor: countColor) + let amountAttributedText = NSMutableAttributedString(string: amountText, font: isSubscription || isSubscriber ? Font.regular(17.0) : Font.semibold(17.0), textColor: countColor) let amount = amount.update( component: BalancedTextComponent( text: .plain(amountAttributedText), @@ -474,9 +577,17 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) )) } else if let toPeer { + let title: String + if isSubscription { + title = strings.Stars_Transaction_Subscription_Subscription + } else if isSubscriber { + title = strings.Stars_Transaction_Subscription_Subscriber + } else { + title = count < 0 || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From + } tableItems.append(.init( id: "to", - title: count < 0 || countIsGeneric ? strings.Stars_Transaction_To : strings.Stars_Transaction_From, + title: title, component: AnyComponent( Button( content: AnyComponent( @@ -529,7 +640,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } tableItems.append(.init( id: "media", - title: strings.Stars_Transaction_Media, + title: isReaction ? strings.Stars_Transaction_Reaction_Post : strings.Stars_Transaction_Media, component: AnyComponent( Button( content: AnyComponent( @@ -568,14 +679,50 @@ private final class StarsTransactionSheetContent: CombinedComponent { )) } + if isSubscription, let additionalDate { + tableItems.append(.init( + id: "additionalDate", + title: strings.Stars_Transaction_Subscription_Status_Subscribed, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: additionalDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + } + + let dateTitle: String + if isSubscription { + if date > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + if isCancelled { + dateTitle = strings.Stars_Transaction_Subscription_Status_Expires + } else { + dateTitle = strings.Stars_Transaction_Subscription_Status_Renews + } + } else { + dateTitle = strings.Stars_Transaction_Subscription_Status_Expired + } + } else if isSubscriber { + dateTitle = strings.Stars_Transaction_Subscription_Status_Subscribed + } else { + dateTitle = strings.Stars_Transaction_Date + } tableItems.append(.init( id: "date", - title: strings.Stars_Transaction_Date, + title: dateTitle, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) + if isSubscriber, let additionalDate { + tableItems.append(.init( + id: "additionalDate", + title: strings.Stars_Transaction_Subscription_Status_Renews, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: additionalDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + } + let table = table.update( component: TableComponent( theme: environment.theme, @@ -589,6 +736,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { let boldTextFont = Font.semibold(15.0) let textColor = theme.actionSheet.secondaryTextColor let linkColor = theme.actionSheet.controlAccentColor + let destructiveColor = theme.actionSheet.destructiveActionTextColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -597,7 +745,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { text: .markdown(text: additionalText, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, - lineSpacing: 0.1, + lineSpacing: 0.2, highlightColor: linkColor.withAlphaComponent(0.2), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { @@ -617,28 +765,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) - - let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonText, - theme: SolidRoundedButtonComponent.Theme(theme: theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, - isLoading: state.inProgress, - action: { - component.cancel(true) - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), - transition: context.transition - ) - + context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: 31.0 + 125.0)) ) @@ -658,8 +785,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } - let textFont = Font.regular(15.0) - let textColor = countOnTop ? theme.list.itemPrimaryTextColor : textColor + let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -699,24 +825,24 @@ private final class StarsTransactionSheetContent: CombinedComponent { originY += description.size.height + 10.0 } - let amountSpacing: CGFloat = 3.0 + let amountSpacing: CGFloat = 1.0 var totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width var amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0 - if isRefund { - let refundText = refundText.update( + if let (statusText, statusColor) = transactionStatus { + let refundText = transactionStatusText.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: strings.Stars_Transaction_Refund, + string: statusText, font: Font.medium(14.0), - textColor: theme.list.itemDisclosureActions.constructive.fillColor + textColor: statusColor )) ), availableSize: context.availableSize, transition: .immediate ) - let refundBackground = refundBackgound.update( + let refundBackground = transactionStatusBackgound.update( component: RoundedRectangle( - color: theme.list.itemDisclosureActions.constructive.fillColor.withAlphaComponent(0.1), + color: statusColor.withAlphaComponent(0.1), cornerRadius: 6.0 ), availableSize: CGSize(width: refundText.size.width + 10.0, height: refundText.size.height + 4.0), @@ -740,14 +866,24 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { originY += amount.size.height + 20.0 } + + let amountLabelOriginX: CGFloat + let amountStarOriginX: CGFloat + if isSubscription || isSubscriber { + amountStarOriginX = amountOriginX + amountStar.size.width / 2.0 + amountLabelOriginX = amountOriginX + amountStar.size.width + amountSpacing + amount.size.width / 2.0 + } else { + amountLabelOriginX = amountOriginX + amount.size.width / 2.0 + amountStarOriginX = amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0 + } + context.add(amount - .position(CGPoint(x: amountOriginX + amount.size.width / 2.0, y: amountOrigin + amount.size.height / 2.0)) + .position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0)) ) context.add(amountStar - .position(CGPoint(x: amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0, y: amountOrigin + amountStar.size.height / 2.0)) + .position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel)) ) - - + context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) ) @@ -758,16 +894,61 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) originY += additional.size.height + 23.0 - let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) - context.add(button - .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) - ) + if let statusText { + originY += 7.0 + let status = status.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: statusText, font: textFont, textColor: statusIsDestructive ? destructiveColor : textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(status + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + status.size.height / 2.0)) + ) + originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0) + } + + if let buttonText { + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: buttonIsDestructive ? SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: destructiveColor) : SolidRoundedButtonComponent.Theme(theme: theme), + font: buttonIsDestructive ? .regular : .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.cancel(true) + if isSubscription { + component.updateSubscription() + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + originY += button.size.height + } context.add(closeButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) ) - let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) + let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) return contentSize } @@ -779,31 +960,31 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let context: AccountContext let subject: StarsTransactionScreen.Subject - let action: () -> Void let openPeer: (EnginePeer) -> Void let openMessage: (EngineMessage.Id) -> Void let openMedia: ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void let openAppExamples: () -> Void let copyTransactionId: (String) -> Void + let updateSubscription: () -> Void init( context: AccountContext, subject: StarsTransactionScreen.Subject, - action: @escaping () -> Void, openPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void, openMedia: @escaping ([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void, openAppExamples: @escaping () -> Void, - copyTransactionId: @escaping (String) -> Void + copyTransactionId: @escaping (String) -> Void, + updateSubscription: @escaping () -> Void ) { self.context = context self.subject = subject - self.action = action self.openPeer = openPeer self.openMessage = openMessage self.openMedia = openMedia self.openAppExamples = openAppExamples self.copyTransactionId = copyTransactionId + self.updateSubscription = updateSubscription } static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { @@ -831,7 +1012,6 @@ private final class StarsTransactionSheetComponent: CombinedComponent { content: AnyComponent(StarsTransactionSheetContent( context: context.component.context, subject: context.component.subject, - action: context.component.action, cancel: { animate in if animate { if let controller = controller() as? StarsTransactionScreen { @@ -848,7 +1028,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { openMessage: context.component.openMessage, openMedia: context.component.openMedia, openAppExamples: context.component.openAppExamples, - copyTransactionId: context.component.copyTransactionId + copyTransactionId: context.component.copyTransactionId, + updateSubscription: context.component.updateSubscription )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, @@ -915,10 +1096,17 @@ private final class StarsTransactionSheetComponent: CombinedComponent { } public class StarsTransactionScreen: ViewControllerComponentContainer { + enum SubscriptionAction { + case cancel + case renew + } + public enum Subject: Equatable { case transaction(StarsContext.State.Transaction, EnginePeer) case receipt(BotPaymentReceipt) case gift(EngineMessage) + case subscription(StarsContext.State.Subscription) + case importer(EnginePeer, StarsSubscriptionPricing, PeerInvitationImportersState.Importer, Double) } private let context: AccountContext @@ -930,7 +1118,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { context: AccountContext, subject: StarsTransactionScreen.Subject, forceDark: Bool = false, - action: @escaping () -> Void + updateSubscription: @escaping (Bool) -> Void = { _ in } ) { self.context = context @@ -939,12 +1127,13 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { var openMediaImpl: (([Media], @escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? var openAppExamplesImpl: (() -> Void)? var copyTransactionIdImpl: ((String) -> Void)? + var updateSubscriptionImpl: (() -> Void)? + super.init( context: context, component: StarsTransactionSheetComponent( context: context, subject: subject, - action: action, openPeer: { peerId in openPeerImpl?(peerId) }, @@ -959,6 +1148,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { }, copyTransactionId: { transactionId in copyTransactionIdImpl?(transactionId) + }, + updateSubscription: { + updateSubscriptionImpl?() } ), navigationBarAppearance: .none, @@ -966,7 +1158,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { theme: forceDark ? .dark : .default ) - self.navigationPresentation = .flatModal + self.navigationPresentation = .standaloneFlatModal self.automaticallyControlPresentationContextLayout = false openPeerImpl = { [weak self] peer in @@ -1070,6 +1262,40 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { HapticFeedback().tap() } + + updateSubscriptionImpl = { [weak self] in + guard let self, case let .subscription(subscription) = subject, let navigationController = self.navigationController as? NavigationController else { + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var titleAndText: (String, String)? + if subscription.flags.contains(.isCancelled) { + updateSubscription(false) + if subscription.untilDate > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + titleAndText = ( + presentationData.strings.Stars_Transaction_Subscription_Renewed_Title, + presentationData.strings.Stars_Transaction_Subscription_Renewed_Text(subscription.peer.compactDisplayTitle).string + ) + } + } else { + if subscription.untilDate < Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + updateSubscription(false) + } else { + updateSubscription(true) + titleAndText = ( + presentationData.strings.Stars_Transaction_Subscription_Cancelled_Title, + presentationData.strings.Stars_Transaction_Subscription_Cancelled_Text(subscription.peer.compactDisplayTitle, stringForMediumDate(timestamp: subscription.untilDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)).string + ) + } + } + + if let (title, text) = titleAndText { + let controller = UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: context, peer: subscription.peer, title: title, text: text, action: nil, duration: 3.0), elevatedLayout: false, position: .bottom, action: { _ in return true }) + Queue.mainQueue().after(0.6) { + navigationController.presentOverlay(controller: controller) + } + } + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index e84f47e28e2..0c806e41fc5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -161,7 +161,7 @@ final class StarsBalanceComponent: Component { let titleFrame = CGRect(origin: CGPoint(x: origin + icon.size.width + spacing, y: contentHeight - 3.0), size: titleSize) titleView.frame = titleFrame - self.icon.frame = CGRect(origin: CGPoint(x: origin, y: contentHeight + 2.0), size: icon.size) + self.icon.frame = CGRect(origin: CGPoint(x: origin, y: contentHeight), size: icon.size) } } contentHeight += titleSize.height diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 61a909dff37..1800b7af0d4 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -217,8 +217,12 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) } else { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) - if item.flags.contains(.isGift) { + if item.flags.contains(.isReaction) { + itemSubtitle = environment.strings.Stars_Intro_Transaction_Reaction_Title + } else if item.flags.contains(.isGift) { itemSubtitle = environment.strings.Stars_Intro_Transaction_Gift_Title + } else if let _ = item.subscriptionPeriod { + itemSubtitle = environment.strings.Stars_Intro_Transaction_SubscriptionFee_Title } else { itemSubtitle = nil } @@ -265,9 +269,15 @@ final class StarsTransactionsListPanelComponent: Component { } itemLabel = NSAttributedString(string: labelString, font: Font.medium(fontBaseDisplaySize), textColor: labelString.hasPrefix("-") ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemDisclosureActions.constructive.fillColor) + var itemDateColor = environment.theme.list.itemSecondaryTextColor itemDate = stringForMediumCompactDate(timestamp: item.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) if item.flags.contains(.isRefund) { itemDate += " – \(environment.strings.Stars_Intro_Transaction_Refund)" + } else if item.flags.contains(.isPending) { + itemDate += " – \(environment.strings.Monetization_Transaction_Pending)" + } else if item.flags.contains(.isFailed) { + itemDate += " – \(environment.strings.Monetization_Transaction_Failed)" + itemDateColor = environment.theme.list.itemDestructiveColor } var titleComponents: [AnyComponentWithIdentity] = [] @@ -298,7 +308,7 @@ final class StarsTransactionsListPanelComponent: Component { text: .plain(NSAttributedString( string: itemDate, font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), - textColor: environment.theme.list.itemSecondaryTextColor + textColor: itemDateColor )), maximumNumberOfLines: 1 ))) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index d0ff4ecdb2e..0b1390aebb8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -18,26 +18,37 @@ import ListSectionComponent import BundleIconComponent import TextFormat import UndoUI +import ListActionItemComponent +import StarsAvatarComponent +import TelegramStringFormatting + +private let initialSubscriptionsDisplayedLimit: Int32 = 3 final class StarsTransactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let starsContext: StarsContext + let subscriptionsContext: StarsSubscriptionsContext let openTransaction: (StarsContext.State.Transaction) -> Void + let openSubscription: (StarsContext.State.Subscription) -> Void let buy: () -> Void let gift: () -> Void init( context: AccountContext, starsContext: StarsContext, + subscriptionsContext: StarsSubscriptionsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, + openSubscription: @escaping (StarsContext.State.Subscription) -> Void, buy: @escaping () -> Void, gift: @escaping () -> Void ) { self.context = context self.starsContext = starsContext + self.subscriptionsContext = subscriptionsContext self.openTransaction = openTransaction + self.openSubscription = openSubscription self.buy = buy self.gift = gift } @@ -116,10 +127,15 @@ final class StarsTransactionsScreenComponent: Component { private var previousBalance: Int64? + private var subscriptionsStateDisposable: Disposable? + private var subscriptionsState: StarsSubscriptionsContext.State? + private var subscriptionsExpanded = false + private var subscriptionsMoreDisplayed: Int32 = 0 + private var allTransactionsContext: StarsTransactionsContext? private var incomingTransactionsContext: StarsTransactionsContext? private var outgoingTransactionsContext: StarsTransactionsContext? - + override init(frame: CGRect) { self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.navigationBackgroundView.alpha = 0.0 @@ -301,6 +317,23 @@ final class StarsTransactionsScreenComponent: Component { self.state?.updated() } }) + + self.subscriptionsStateDisposable = (component.subscriptionsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + let isFirstTime = self.subscriptionsState == nil + if !state.subscriptions.isEmpty { + self.subscriptionsState = state + } else { + self.subscriptionsState = nil + } + + if !self.isUpdating { + self.state?.updated(transition: isFirstTime ? .immediate : .spring(duration: 0.4)) + } + }) } var wasLockedAtPanels = false @@ -574,17 +607,138 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += balanceSize.height contentHeight += 44.0 - let subscriptionsItems: [AnyComponentWithIdentity] = [] + let fontBaseDisplaySize = 17.0 + var subscriptionsItems: [AnyComponentWithIdentity] = [] + if let subscriptionsState = self.subscriptionsState { + var subscriptions = subscriptionsState.subscriptions + var limit: Int32 + if self.subscriptionsExpanded { + limit = 25 + self.subscriptionsMoreDisplayed + } else { + limit = initialSubscriptionsDisplayedLimit + } + subscriptions = Array(subscriptions.prefix(Int(limit))) + + for subscription in subscriptions { + var titleComponents: [AnyComponentWithIdentity] = [] + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: subscription.peer.compactDisplayTitle, + font: Font.semibold(fontBaseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + let dateText: String + let dateValue = stringForDateWithoutYear(date: Date(timeIntervalSince1970: Double(subscription.untilDate)), strings: environment.strings) + var isExpired = false + if subscription.untilDate > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + if subscription.flags.contains(.isCancelled) { + dateText = environment.strings.Stars_Intro_Subscriptions_Expires(dateValue).string + } else { + dateText = environment.strings.Stars_Intro_Subscriptions_Renews(dateValue).string + } + } else { + dateText = environment.strings.Stars_Intro_Subscriptions_Expired(dateValue).string + if !subscription.flags.contains(.isCancelled) { + isExpired = true + } + } + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: dateText, + font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ) + let labelComponent: AnyComponentWithIdentity + if subscription.flags.contains(.isCancelled) || isExpired { + labelComponent = AnyComponentWithIdentity(id: "cancelledLabel", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: isExpired ? environment.strings.Stars_Intro_Subscriptions_ExpiredStatus : environment.strings.Stars_Intro_Subscriptions_Cancelled, font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemDestructiveColor))) + )) + } else { + let itemLabel = NSAttributedString(string: "\(subscription.pricing.amount)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor) + let itemSublabel = NSAttributedString(string: environment.strings.Stars_Intro_Subscriptions_PerMonth, font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor) + + labelComponent = AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel, subtext: itemSublabel))) + } + + subscriptionsItems.append(AnyComponentWithIdentity( + id: subscription.id, + component: AnyComponent( + ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: .peer(subscription.peer), photo: nil, media: [], backgroundColor: environment.theme.list.plainBackgroundColor))), false), + icon: nil, + accessory: .custom(ListActionItemComponent.CustomAccessory(component: labelComponent, insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), + action: { [weak self] _ in + guard let self, let component = self.component else { + return + } + component.openSubscription(subscription) + } + ) + ) + )) + } + if subscriptionsState.canLoadMore { + subscriptionsItems.append(AnyComponentWithIdentity( + id: "showMore", + component: AnyComponent( + ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(Text( + text: environment.strings.Stars_Intro_Subscriptions_ShowMore, + font: Font.regular(17.0), + color: environment.theme.list.itemAccentColor + )), + leftIcon: .custom( + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image( + image: PresentationResourcesItemList.downArrowImage(environment.theme), + size: CGSize(width: 30.0, height: 30.0) + )) + ), + false + ), + accessory: nil, + action: { [weak self] _ in + guard let self, let component = self.component else { + return + } + if self.subscriptionsExpanded { + self.subscriptionsMoreDisplayed += 10 + } else { + self.subscriptionsExpanded = true + } + self.state?.updated(transition: .spring(duration: 0.4)) + component.subscriptionsContext.loadMore() + }, + highlighting: .default, + updateIsHighlighted: { view, _ in + + }) + ) + )) + } + } if !subscriptionsItems.isEmpty { - //TODO:localize let subscriptionsSize = self.subscriptionsView.update( - transition: .immediate, + transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "My Subscriptions".uppercased(), + string: environment.strings.Stars_Intro_Subscriptions_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -754,22 +908,32 @@ final class StarsTransactionsScreenComponent: Component { public final class StarsTransactionsScreen: ViewControllerComponentContainer { private let context: AccountContext private let starsContext: StarsContext + private let subscriptionsContext: StarsSubscriptionsContext private let options = Promise<[StarsTopUpOption]>() + private let navigateDisposable = MetaDisposable() + public init(context: AccountContext, starsContext: StarsContext, forceDark: Bool = false) { self.context = context self.starsContext = starsContext + self.subscriptionsContext = context.engine.payments.peerStarsSubscriptionsContext(starsContext: starsContext) + var buyImpl: (() -> Void)? var giftImpl: (() -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? + var openSubscriptionImpl: ((StarsContext.State.Subscription) -> Void)? super.init(context: context, component: StarsTransactionsScreenComponent( context: context, starsContext: starsContext, + subscriptionsContext: self.subscriptionsContext, openTransaction: { transaction in openTransactionImpl?(transaction) }, + openSubscription: { subscription in + openSubscriptionImpl?(subscription) + }, buy: { buyImpl?() }, @@ -796,6 +960,36 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { }) } + openSubscriptionImpl = { [weak self] subscription in + guard let self else { + return + } + let controller = context.sharedContext.makeStarsSubscriptionScreen(context: context, subscription: subscription, update: { [weak self] cancel in + guard let self else { + return + } + if subscription.untilDate > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + var updated = false + if let channel = subscription.peer._asPeer() as? TelegramChannel, channel.participationStatus == .left && !subscription.flags.contains(.isCancelled) { + let _ = self.context.engine.payments.fulfillStarsSubscription(peerId: context.account.peerId, subscriptionId: subscription.id).startStandalone() + updated = true + } + if !updated { + if subscription.flags.contains(.isCancelled) { + self.subscriptionsContext.updateSubscription(id: subscription.id, cancel: false) + } else { + self.subscriptionsContext.updateSubscription(id: subscription.id, cancel: true) + } + } + } else { + if let inviteHash = subscription.inviteHash { + self.context.sharedContext.handleTextLinkAction(context: self.context, peerId: nil, navigateDisposable: self.navigateDisposable, controller: self, action: .tap, itemLink: .url(url: "https://t.me/+\(inviteHash)", concealed: false)) + } + } + }) + self.push(controller) + } + buyImpl = { [weak self] in guard let self else { return @@ -806,7 +1000,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic(requiredStars: nil), completion: { [weak self] stars in + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in guard let self else { return } @@ -902,13 +1096,18 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { } self.starsContext.load(force: false) + self.subscriptionsContext.loadMore() } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override public func viewDidLoad() { - super.viewDidLoad() + deinit { + self.navigateDisposable.dispose() + } + + public func update() { + self.subscriptionsContext.loadMore() } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 913a0eff7ea..d6384438a6a 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -28,7 +28,8 @@ private final class SheetContent: CombinedComponent { let invoice: TelegramMediaInvoice let source: BotPaymentInvoiceSource let extendedMedia: [TelegramExtendedMedia] - let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError> + let navigateToPeer: (EnginePeer) -> Void let dismiss: () -> Void init( @@ -37,7 +38,8 @@ private final class SheetContent: CombinedComponent { invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void, dismiss: @escaping () -> Void ) { self.context = context @@ -46,6 +48,7 @@ private final class SheetContent: CombinedComponent { self.source = source self.extendedMedia = extendedMedia self.inputData = inputData + self.navigateToPeer = navigateToPeer self.dismiss = dismiss } @@ -74,9 +77,11 @@ private final class SheetContent: CombinedComponent { private(set) var botPeer: EnginePeer? private(set) var chatPeer: EnginePeer? + private(set) var authorPeer: EnginePeer? private var peerDisposable: Disposable? private(set) var balance: Int64? private(set) var form: BotPaymentForm? + private(set) var navigateToPeer: (EnginePeer) -> Void private var stateDisposable: Disposable? @@ -96,13 +101,15 @@ private final class SheetContent: CombinedComponent { source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], invoice: TelegramMediaInvoice, - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void ) { self.context = context self.starsContext = starsContext self.source = source self.extendedMedia = extendedMedia self.invoice = invoice + self.navigateToPeer = navigateToPeer super.init() @@ -125,6 +132,7 @@ private final class SheetContent: CombinedComponent { self.form = inputData?.1 self.botPeer = inputData?.2 self.chatPeer = chatPeer + self.authorPeer = inputData?.3 self.updated(transition: .immediate) if self.optionsDisposable == nil, let balance = self.balance, balance < self.invoice.totalAmount { @@ -159,6 +167,7 @@ private final class SheetContent: CombinedComponent { return } + let navigateToPeer = self.navigateToPeer let action = { [weak self] in guard let self else { return @@ -167,8 +176,19 @@ private final class SheetContent: CombinedComponent { self.updated() let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source) - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } completion(true) + if case let .starsChatSubscription(link) = self.source { + let _ = (self.context.engine.peers.joinLinkInformation(link) + |> deliverOnMainQueue).startStandalone(next: { result in + if case let .alreadyJoined(peer) = result { + navigateToPeer(peer) + } + }) + } }, error: { [weak self] error in guard let self else { return @@ -235,7 +255,7 @@ private final class SheetContent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, starsContext: self.starsContext, source: self.source, extendedMedia: self.extendedMedia, invoice: self.invoice, inputData: self.inputData) + return State(context: self.context, starsContext: self.starsContext, source: self.source, extendedMedia: self.extendedMedia, invoice: self.invoice, inputData: self.inputData, navigateToPeer: self.navigateToPeer) } static var body: Body { @@ -248,6 +268,7 @@ private final class SheetContent: CombinedComponent { let balanceTitle = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self) let balanceIcon = Child(BundleIconComponent.self) + let info = Child(BalancedTextComponent.self) return { context in let environment = context.environment[EnvironmentType.self] @@ -261,7 +282,7 @@ private final class SheetContent: CombinedComponent { var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let background = background.update( - component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0), + component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0), availableSize: CGSize(width: context.availableSize.width, height: 1000.0), transition: .immediate ) @@ -281,13 +302,19 @@ private final class SheetContent: CombinedComponent { } else { subject = .none } + + var isSubscription = false + if case .starsChatSubscription = context.component.source { + isSubscription = true + } let star = star.update( component: StarsImageComponent( context: component.context, subject: subject, theme: theme, diameter: 90.0, - backgroundColor: theme.list.blocksBackgroundColor + backgroundColor: theme.actionSheet.opaqueItemBackgroundColor, + icon: isSubscription ? .star : nil ), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition @@ -321,8 +348,15 @@ private final class SheetContent: CombinedComponent { contentSize.height += 126.0 + let titleString: String + if isSubscription { + titleString = strings.Stars_Transfer_Subscribe_Channel_Title + } else { + titleString = strings.Stars_Transfer_Title + } + let title = title.update( - component: Text(text: strings.Stars_Transfer_Title, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), + component: Text(text: titleString, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) @@ -342,7 +376,9 @@ private final class SheetContent: CombinedComponent { let amount = component.invoice.totalAmount let infoText: String - if !component.extendedMedia.isEmpty { + if case .starsChatSubscription = context.component.source { + infoText = strings.Stars_Transfer_SubscribeInfo(state.botPeer?.compactDisplayTitle ?? "", strings.Stars_Transfer_Info_Stars(Int32(amount))).string + } else if !component.extendedMedia.isEmpty { var description: String = "" var photoCount: Int32 = 0 var videoCount: Int32 = 0 @@ -368,11 +404,26 @@ private final class SheetContent: CombinedComponent { description += "**\(strings.Stars_Transfer_SingleVideo)**" } } - infoText = strings.Stars_Transfer_UnlockInfo( - description, - state.chatPeer?.compactDisplayTitle ?? "", - strings.Stars_Transfer_Info_Stars(Int32(amount)) - ).string + + if let authorPeerName = state.authorPeer?.compactDisplayTitle { + infoText = strings.Stars_Transfer_UnlockBotInfo( + description, + authorPeerName, + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string + } else if let botPeerName = state.botPeer?.compactDisplayTitle { + infoText = strings.Stars_Transfer_UnlockBotInfo( + description, + botPeerName, + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string + } else { + infoText = strings.Stars_Transfer_UnlockInfo( + description, + state.chatPeer?.compactDisplayTitle ?? "", + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string + } } else { infoText = strings.Stars_Transfer_Info( component.invoice.title, @@ -446,7 +497,12 @@ private final class SheetContent: CombinedComponent { } let amountString = presentationStringsFormattedNumber(Int32(amount), presentationData.dateTimeFormat.groupingSeparator) - let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let buttonAttributedString: NSMutableAttributedString + if case .starsChatSubscription = component.source { + buttonAttributedString = NSMutableAttributedString(string: strings.Stars_Transfer_Subscribe, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + } else { + buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + } if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) @@ -484,7 +540,7 @@ private final class SheetContent: CombinedComponent { } else if let peerId = state?.botPeer?.id { purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount) } else { - purpose = .generic(requiredStars: nil) + purpose = .generic } let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen( context: accountContext, @@ -506,8 +562,12 @@ private final class SheetContent: CombinedComponent { }, completion: { [weak controller] success in if success { let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } + var title = presentationData.strings.Stars_Transfer_PurchasedTitle let text: String - if let _ = component.invoice.extendedMedia { + if isSubscription { + title = presentationData.strings.Stars_Transfer_Subscribe_Successful_Title + text = presentationData.strings.Stars_Transfer_Subscribe_Successful_Text(presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount)), botTitle).string + } else if let _ = component.invoice.extendedMedia { text = presentationData.strings.Stars_Transfer_UnlockedText( presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string } else { text = presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string @@ -518,18 +578,11 @@ private final class SheetContent: CombinedComponent { if let lastController = navigationController.viewControllers.last as? ViewController { let resultController = UndoOverlayController( presentationData: presentationData, -// content: .image( -// image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!, -// title: presentationData.strings.Stars_Transfer_PurchasedTitle, -// text: text, -// round: false, -// undoText: nil -// ), content: .universal( animation: "StarsSend", scale: 0.066, colors: [:], - title: presentationData.strings.Stars_Transfer_PurchasedTitle, + title: title, text: text, customUndoText: nil, timeout: nil @@ -559,6 +612,50 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height + + if isSubscription { + contentSize.height += 14.0 + + let termsTextFont = Font.regular(13.0) + let termsTextColor = theme.actionSheet.secondaryTextColor + let termsLinkColor = theme.actionSheet.controlAccentColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsTextFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsTextFont, textColor: termsLinkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let info = info.update( + component: BalancedTextComponent( + text: .markdown( + text: strings.Stars_Subscription_Terms, + attributes: termsMarkdownAttributes + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak controller] attributes, _ in + if let controller, let navigationController = controller.navigationController as? NavigationController { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Subscription_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + } + ), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(info + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + info.size.height / 2.0)) + ) + contentSize.height += info.size.height + + } + contentSize.height += 48.0 return contentSize @@ -574,7 +671,8 @@ private final class StarsTransferSheetComponent: CombinedComponent { private let invoice: TelegramMediaInvoice private let source: BotPaymentInvoiceSource private let extendedMedia: [TelegramExtendedMedia] - private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError> + private let navigateToPeer: (EnginePeer) -> Void init( context: AccountContext, @@ -582,7 +680,8 @@ private final class StarsTransferSheetComponent: CombinedComponent { invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void ) { self.context = context self.starsContext = starsContext @@ -590,6 +689,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { self.source = source self.extendedMedia = extendedMedia self.inputData = inputData + self.navigateToPeer = navigateToPeer } static func ==(lhs: StarsTransferSheetComponent, rhs: StarsTransferSheetComponent) -> Bool { @@ -623,6 +723,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { source: context.component.source, extendedMedia: context.component.extendedMedia, inputData: context.component.inputData, + navigateToPeer: context.component.navigateToPeer, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { @@ -681,8 +782,9 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, - extendedMedia: [TelegramExtendedMedia], - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + extendedMedia: [TelegramExtendedMedia] = [], + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, + navigateToPeer: @escaping (EnginePeer) -> Void = { _ in }, completion: @escaping (Bool) -> Void ) { self.context = context @@ -697,7 +799,8 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { invoice: invoice, source: source, extendedMedia: extendedMedia, - inputData: inputData + inputData: inputData, + navigateToPeer: navigateToPeer ), navigationBarAppearance: .none, statusBarStyle: .ignore, diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 0270432457a..7a03e2b91c6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -55,7 +55,7 @@ private final class SheetContent: CombinedComponent { let background = Child(RoundedRectangle.self) let closeButton = Child(Button.self) let title = Child(Text.self) - let urlSection = Child(ListSectionComponent.self) + let amountSection = Child(ListSectionComponent.self) let button = Child(ButtonComponent.self) let balanceTitle = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self) @@ -246,7 +246,7 @@ private final class SheetContent: CombinedComponent { amountFooter = nil } - let urlSection = urlSection.update( + let amountSection = amountSection.update( component: ListSectionComponent( theme: theme, header: AnyComponent(MultilineTextComponent( @@ -283,19 +283,19 @@ private final class SheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), transition: context.transition ) - context.add(urlSection - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) + context.add(amountSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amountSection.size.height / 2.0)) .clipsToBounds(true) .cornerRadius(10.0) ) - contentSize.height += urlSection.size.height + contentSize.height += amountSection.size.height contentSize.height += 32.0 let buttonString: String if case .paidMedia = component.mode { buttonString = environment.strings.Stars_PaidContent_Create } else if let amount = state.amount { - buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(amount)" + buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(Int32(amount), environment.dateTimeFormat.groupingSeparator))" } else { buttonString = environment.strings.Stars_Withdraw_Withdraw } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 65dfe5baa3d..8a79e94a43e 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -2734,7 +2734,8 @@ final class StorageUsageScreenComponent: Component { navigationController: navigationController, dismissInput: { [weak self] in self?.endEditing(true) - }, present: { [weak self] c, a in + }, + present: { [weak self] c, a, _ in guard let self else { return } @@ -2806,6 +2807,7 @@ final class StorageUsageScreenComponent: Component { } let _ = self }, openBotCommand: { _ in + }, openAd: { _ in }, addContact: { _ in }, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in guard let self else { diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 7c2d2b3aa8b..fa948e4cedd 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -987,6 +987,9 @@ public final class PeerListItemComponent: Component { self.file = file self.updateReactionLayer() }) + case .stars: + self.file = reaction.file + self.updateReactionLayer() } } else { self.file = nil diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index a0f38bbe683..cb8c039a8e3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -1859,6 +1859,8 @@ public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) - if !customReactions.contains(fileId) { customReactions.append(fileId) } + case .stars: + break } } if !builtinReactions.isEmpty { @@ -2090,6 +2092,8 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine if !customReactions.contains(fileId) { customReactions.append(fileId) } + case .stars: + break } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 6e447801155..178920de707 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -1655,7 +1655,7 @@ private final class StoryContainerScreenComponent: Component { } if case let .user(user) = slice.peer, user.botInfo != nil { - //TODO:localize + //TODO:release let _ = component.context.engine.messages.deleteBotPreviews(peerId: slice.peer.id, language: nil, media: [slice.item.storyItem.media._asMedia()]).startStandalone() } else { let _ = component.context.engine.messages.deleteStories(peerId: slice.peer.id, ids: [slice.item.storyItem.id]).startStandalone() @@ -2198,6 +2198,8 @@ func allowedStoryReactions(context: AccountContext) -> Signal<[ReactionItem], No largeApplicationAnimation: nil, isCustom: true )) + case .stars: + break } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index 283d05ac45a..6ce4ea56fcd 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -98,6 +98,8 @@ public func storyPreviewWithAddedReactions( if !customFileIds.contains(fileId) { customFileIds.append(fileId) } + case .stars: + break } } } @@ -361,6 +363,15 @@ final class StoryItemOverlaysView: UIView { }) } } + case .stars: + if let availableReactions { + for reactionItem in availableReactions.reactionItems { + if reactionItem.reaction.rawValue == reaction { + file = reactionItem.stillAnimation + break + } + } + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e4b726ed6c5..2f213613972 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1745,6 +1745,15 @@ public final class StoryItemSetContainerComponent: Component { } case let .custom(fileId): animationFileId = fileId + case .stars: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == value { + centerAnimation = availableReaction.listAnimation + break + } + } + } } if animationFileId == nil && centerAnimation == nil { @@ -2944,6 +2953,15 @@ public final class StoryItemSetContainerComponent: Component { } case let .custom(fileId): animationFileId = fileId + case .stars: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == value { + centerAnimation = availableReaction.listAnimation + break + } + } + } } if animationFileId == nil && centerAnimation == nil { @@ -4564,7 +4582,7 @@ public final class StoryItemSetContainerComponent: Component { standaloneReactionAnimation.frame = self.bounds self.addSubview(standaloneReactionAnimation.view) - }, completion: { [weak targetView, weak reactionContextNode] in + }, onHit: nil, completion: { [weak targetView, weak reactionContextNode] in targetView?.removeFromSuperview() if let reactionContextNode { reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in @@ -4602,6 +4620,8 @@ public final class StoryItemSetContainerComponent: Component { } } } + case .stars: + break } let message: EnqueueMessage = .message( @@ -4748,7 +4768,7 @@ public final class StoryItemSetContainerComponent: Component { standaloneReactionAnimation.frame = self.bounds self.componentContainerView.addSubview(standaloneReactionAnimation.view) - }, completion: { [weak reactionContextNode] in + }, onHit: nil, completion: { [weak reactionContextNode] in if let reactionContextNode { reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak reactionContextNode] _ in reactionContextNode?.view.removeFromSuperview() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index e7a36f29aec..bb576287101 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -3577,6 +3577,15 @@ final class StoryItemSetContainerSendMessage { animateWithReactionItem(reactionItem) } }) + case .stars: + if let availableReactions = component.availableReactions { + for reactionItem in availableReactions.reactionItems { + if reactionItem.reaction.rawValue == reaction { + animateWithReactionItem(reactionItem) + break + } + } + } } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 29ea4a47263..843fe4d68ba 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -569,6 +569,15 @@ final class StoryItemSetViewListComponent: Component { if case let .view(view) = item { animationFile = view.reactionFile } + case .stars: + if let availableReactions = component.availableReactions { + for availableReaction in availableReactions.reactionItems { + if availableReaction.reaction.rawValue == reaction { + animationFile = availableReaction.listAnimation + break + } + } + } } return PeerListItemComponent.Reaction( reaction: reaction, diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json new file mode 100644 index 00000000000..3ec814d17a4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "linklink_40.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/linklink_40.pdf b/submodules/TelegramUI/Images.xcassets/Item List/InviteLink.imageset/linklink_40.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c2be8a1a21eb38d217f45452f8b9f073b848d022 GIT binary patch literal 1437 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

Z;b|mIYCO_V!PLq2ICZhb$WhE7C%42H| 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 Q)`w^e8ZD%tU|?qWUkDIBVE_OC literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/BalanceStar.imageset/balancestar_48 (2).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/BalanceStar.imageset/balancestar_48 (2).pdf deleted file mode 100644 index 323a72ad266b04a1f1d299d555acd011ece78ace..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14697 zcmeHucRbba`+sF5gp4wZgAl@*j$>xa4p|u;GwU3Vkr6_K%utk(y+V}TpkyVZ?3o=I z6_Wis$F6sucYWTU-{ zGdm(OjwoXX3p7f@#nu!6#nuxQCAN3Km?CY6-2i)9O`VOe{Oz5IqwQX2%^d)4fXynl zXxm?0{2*-aXtY<;#?g9D4bsG9tMY$a-W_0J2G~*pLBIqc5Wt^Vur2(@7X0_*_gMe~ z1OMEDZQ(z*;J+uo&jNnvzcB{e!hdYRe@}kbg5A0LI~45-{&sq8wvC%i?!=}jlkEwL zy}~Ub#JGiHYc)ZD1aQwcOCh_Z`_>rfAFVMvi^G4se(Ww*n+EtN#`+I1p^%1%d$X;sv%_io=VHg|&kz z2K#G`bTE}NHAb75Zf`0Q5@_fBTgt9tsKCx1wRg3HBsEiev?IpY)P8frQ$;#p8%J$j zgEoJZkPa9NT)RAADAuzu2tO3S1LuW-K@bEOuzRuZDFng`6o5fN2!H^PR{#M5Lg0XH zb(?$W-+l3^UqPB!pv*T_OQO*j6Z>D9wCn3#BmY#xe(ijcXlpb^-4ogJ`C7S{a^ z7)R5suYcRg*h9;2d%7vWw++32tGj%g(mPfEM3bl}_8f&fN^L1scQkUqov-+0&={NT zvh713|K2$A$zsshQxtCYZ7JNm|5+WMI`%OJiL$pvVoXuSe?bwSBv#;GfNZxzTkUJB z7+IPcJ76{J9r~OPI>x-sJqe@mU7Vv{J0!r{GnxRCvkrGY1SLMiS?4V!_f%xyiz54C zQJ?k4OT(g~rX5Y0jW-QN*H;$ThVwR7zYKhtn=K|suRryCrsF^Ng4M(G`^3WhGLNU{ znZzekW;JVbZ)L6|k7$R|TYib1s_1+rS6bvWPhsWwrY4Yh<7n5K+=^l|2kRoE4;@yZ?N$pI;Vi-1kJ~>H=Xsg;Y3sjU#Rd!Axd#Iox~3>Cxf&7PTh>g zDEPMBZ6^?~#n;a(44P1w8Id~_Lj?npbM2-ltRoupz{wt#i;{SIGCWb|TVcfF%-iicwBjI&D$qyI&w`o#47grGN{ zs9?SqEU!|~ml{y!VkEH;s_-%>NIGidK0)r zM+o#iTc$|xD^cc1)j_Y8l9A}AZ@Pt!=$XyzMVLfuE*1nHH^%|b#A9Isa>u6AmP7NS?ZGuF zu@|e4E!kO!;3?`^B18%lLLKhi%YIqOTtm}8C{3o3?57s7pf_#Y}Mq> za;k67eAgM&!?J!j&-p@fEG$idG1G+VIOl_}s`52!5_2sQ43rXgt7al2#1+J~8r`z% zGAwEuLxqGJd#jxLTqdR0vvmWY^X-Ej41IwPZIyyOMu>?Tp=Zz45}Y+6$Hg0))?@^N85zwXsOKF3e34a5}B1>Ogwx*N-I88%mWC8vAF1pW}fe}jur5WnPmEvu2sktrz0p*p<={M@LY_yVu^rnj? z&0a_he@+ofV>`b{g^)UAPkiwDsuF(wPJr?WNX;@_Rb=iJ2!a_%X~

c%1k7a=?^}*aJfv0r1gyL3)61E(d$|W zFL~NGe46d|h(P+9gsv2$F_Z_5OIUj|Ude~OPKco;kpvx9FSobI>qDHW3saUJzebV> zW)P94vT=;($@PyHFFcVKhb+{3D|z%dYoF-5IbitP8!YSwybio(1P;ebm7fc9gq714 zhBt@acBUrf01uu|Nv{=7D(poj)->b$rApPY8&J^`obESR9gNisA)kD+p42E|85E>w z<#5b7L~$uG8!0gwND%P)t?g$pvlLCqhw!_BtdrCdajoY(b>gpW>Y#w&CLB zJv6O&y7PE3$52e7KicW0z_Rb;V`aIlBz!^QxEu|qd_dDy{H6)DR;I(oJ(ROKuhp}r z(^7rLyQE@rgefuXfFI_;2(uFg^0cBl&tLdjGH}{V(kYO$#g##O&QWJvgm;kup z;SYtz<(pIITk^7MP94GI8C?#$H=W-vt`{&Ern%hVF&~3u(H0yhsOafE+T?$Li=4Y9 zunA3~-J&FQvi+r&`Lj?PRjH4sdk@0!F9h0rA;wKo&sPW5dzIR?gQ%|%>qYsmEfV*4 z+x9(Z?9=S6G3!*-Q}m~nXJ=Nt!TIR0#|nkHE8dR+Sy z7A=`n>TnZc0004+YAGFAttm?lW3)FKnJAd;jlQ4u=y#{QYQo9LZPL8@gj^3er1czl zRnFUZNRt1EcYvMhkvIGl5k&f8X{s;fF7}Eahl(Go=6%aV^4_dxtx14p|xp6 zy#jH$n}TKftrq!EocKXf{Pj;OSSZqHxY=CpBC(6rzjsGjM$Gp_UkpWXvq#L8X+bAZ z@}~x=S#zh5OGZB7PMv4)8mV($az6En9N}Ihe?Lj;%q`Zk{<()~;JN$i_^tKIudmQf z*PeNnuDPsWR+zqhdUWj<$p_p4b}N_!!}dj!@ZI9Sk0yUUiDD1TJNZz7|L;fR^PAxX z_Po3I**y~f>Wuz>QR|OUtD3CD56;_j8M{N0H>H|NTq z#@PQbX#Lgr@?k@CS#<|&B4{%>mEOwOs4F4ut@cH$xZ7XV{+DKLJ>ZiC;4)AD2o~5# zSJBbN0)@7>aQI6g{2RTy(fRM>u+O)Bx$A~MGjm@CV4seiQRn-&^JqT=>0NcVjZ4D` znnhlCf8wX|9Yi`aHLuJ>wHE-7-f93x$Zwzp$-u0SzfDedm5;T{Q zIbiQGB5cmHI9sg8gLv}N&XUDtj8!T7}7giWBkOG(Pf&G=dOwVWa<2H{QKmn z_BSPqvRUI(6YF-YXA2v2k6dDe^xR`yx{}kl;+*n0euAGJBf*D;#_%B`SdEe0H@*Xrjb;`^lbb|14zk++pyAY>} zvwixf+9Y*#x!wEg=zXi{3UnIdr}A^p3C@f_F&}7BQRw`<|9kGE@NOF z--oe}I?Cz-lRK3byH@NZ_(W5?xPyD2<*ywHnC_Xf>2H$#wgAWqja3tcSbb@+h4{x^ zXu>lxEz-S}OgAStO7xh(>D23I?3Y67KdyQa`a>PuO(Y3xqk4FxhpZ0Po;cU*=(`H-{R$!$`mLw{@`>0m-MsMYO zgr|N&qKtxj&i$9;+Pv1GQ{*!2)3A$b;KhlH%}h_MBg$)KqOVWQSrf{Kw(8!Snadn6 z4kOMYYD$0`-Pg#ze5C8sn2EY|IvXAx|8mKxQMmYnR4faDkR* z-^iN8Z2 zU#KRnWgcL6ZmXfoW=VG( zV;1jx^asXEO(uLb&8`qApVA15EHXz>YwCH%zY|J%E zsg;S18dV@;eXdg>?||2v%HCAcf|zCZ*vmA^OFS6$*0)3r=u2IWvs7Q&Ad#;O?mt=b zxF71$I2v|EBW9{c)2WZaUtK!V!8az&;Ue$zOLROk@d-wzW(A$@QTmI7NvKq_Jl+)A zD9jVGh(V1zEz(gh)kDkUiwhf~!d|Ccqo>sTqIi>$DtBF?OkBIR zZ2Iw=BjGxsm5N~&TG1Zo?KP_;Lith{Zuma@_)PEtU_w*MHoT~^?TK+!lEO3MWdm1n zA`|h&$s|qL$M1;l#M!r+>ST_d#ANEM`Qmi7Ovdx@Ur|tQSXfJIpl%BUijl=h<}81c zJk#iL3{`a467%`7tn2}&HQKDRSr{T0FW|V2^!sA0#_Jrp-gHM#a-JU#pVu~XCaq)D zE97&(&{yol-D^#*aY_r_+7pmYswQ@P(i%kD%c4a%@qqE^MlOEfQzl}8c4?y4t?c5(-&JCzrbNbeR}K`kj|I)<6f~@F^hdk~_kyIE8918Ratg^VUaU|UCPU%KKPS&j|k>`=m@y_ZM>V-lXCdlv6 z3zEa{d^hGD8djhtlDPI<9*b{oPD6*e2=_XIQ>FZR1OeYiEZhbi-5$u)xv;tppgH^u zE$7Lt*|?SOOnNa)rpe|Ur+p=smKaBc0uESPxq)@g? z+R;{4HIbi9JT$!teCG;R9qCS5bvvKsK*@HaIWktmp)C7wqIFTOPs4#g@74eX(L_Sph;#uM4Ko=y(R=w z9pgL@V#nm6)j;vO-xk~L;USTL4a{0ZTY3j z-jAj8o*n0^c-BP9h+YUJzBzD%USh^PMCDz31eN!cIwhoX>YCcEqZy5qlWw;g$yTMh z%a?T4Wh>$54vjmub3leVX8M@6|oy!3u!z+ngF72&uz?diJVX=UIHqM)+DC zICSK8Jj=7wb8-(yB}0W<@P!{%M}#}`2oRqx=UYUkXNRb?@r~b{Je~z&hUQGL%nN6K zP0l`sEKT&S8lv+fKgL4uMWSLLb23xipRw#XRa;nAxcm*mb8ctoR0_E=xxG>Blm%}J zPYjj&C4L`s2ps0iYI2ka6=@-LyV%hK68FFd)31qdtsLf0Da_{b{&*)zP)`Dc{o4_b;Df zhjQS`gQ*yYi{C~pE(9!tsOl;K9<;G82d1n-k~?I2j-q0VB@M=1sum@v-&rWyl7q!a z{ENck=YOHXFgwYPM)R5RVqY0e$ z=&+x7(QY2srXVgri2os~;`mjM$&uCa&YF#VU_;+c%W!6K7lpF4& zS63g@~$Q0F@2i~K53G-*}U@l5vU%xY50TDO2Hz?7>Tj?70?5*RrCc3eOh_@XwmtPDR>ph5@?D7;%vSj= zQc(=esPy=zOKBA9Sqo&ocsZ%^-N0o^D>xeqPG82kebiNw2=sSp%Mk!N>`=-I>C;MMGUX=2ADL@z(Fy)E~^bNJ4kT zx6N$m?WY6JoVroA!%t5Y%xN&S>GEbImng52o*(Ne$$S4)aph!z?pyOLTHRX*JKw@Z zSr2lBMwJYGvhkuuQ`)86)Ee(papGbTJi?U0EXUb3stJU<@~*+@n-kEML9a6^{40Vf z^F8w(StWlWbc}O)?anCUXBR_3BOgH#4ummKAT2#=kA@q9U7@xqeQ{dU@9{Iu^&KY0 zFUf`>sB;KrKiglG0i8jce0dCh&id#(T&jIzP?UmmE^ScZ%i{aHLq~ZWhl%K74OeV^ z&w32=j)gq6*mwhuqb@$gf@c3}Y8- z?oQ5*bAKRxc$C@V#Zy=Rt3HQF@i*q&M@W?N9@w=Lw(Hd9Cj|OlS58ZKr*QRnQb2bC zOO8mxjLQf6?5vqngI|p#Gfq|WRexDAqi(Dax^Ok7O4pI;MHcM*a$_#eU?>l}yx z1Us}_1U1;!`tVbaendGC3oF2fgFyCB{v&~Lo2;^#aMT6?aPJq?c5`??XDGp&V#dflF7Zy< zZC@r1m)P6RWlAUk_|(y64vH4$R~&X*-6y+KeJ2&W*}m$QnqBG|6wC`mK;Z&F01U(n zLO}Q-KmgC)<=$hk0IvW90f!1;*=}&`;*Y?t(tpEU+u>MkD*Z38)qcTG`_oMSUTz<^ z_x@QD_dlng?KtG$NQmPQ@(*ZcJLA3&3OkoOQ20$J{v;3oD=mDgKM^A#2;624LI42} zEV{5K1TX}^vv;}o7!2eE!C(k}Fcy4R7{Xw{zavKO`0{@mT$?-I|DkIKdV9bB2wj_S z!+z~B#X)<7;J<%ayUkey?YywTwy{k`cRn~BRgPkprtqC}2@D9oa`mF4zM!vuIB!^3 zlHnMO*RiR<@$u~N4jBqmLFH`KG^Ec_C_RL!UwD1()AW~e^mD-%>+8aXyks!` z2>>a&q91%Zk;}r;Tt**u1_@K<>J7IjSGhoqFH@oq>2SQq#-2{g>EJ@48f7MDmELN5 zqMJRX8_02Nwod!D#=$X-2&sW|G5Tj?#H^~Xbqj0bA3WYrQkMh2ci#i0D_TN1Ti1~;o7qFIyDu*c>_Z^Q?wv)lBA z)HO6jHnh@&->H)1Gz!BkJW8j*JM2?N>bj6vpvj+?-D?z2Z#)A#-*L>!(e!8;G4xf= zl0h?BWYjGgQ|m8WUkM+FbLx|xouV04JdHG)Aj|R!HzBi2oFMy>4(Oa+zsMLH6z3io3(d_Z<6D&3#*m3Da zV{9Fl52aqDhmLwo(X*(^)wogo`)*D$!1G3ny}`j?uXXj4gsP2cEUJEQ-@(dG(ilm^ z)xbGuNo9K*ZgF`DRC-LaSu=n#b5D59#udDltCfDmM%~0v^3vxzg+Qa1#2cErxN4Fh zLQv7d1L8m^n8mZUZR^X8&T~H zZW3`tbi(1oU!R{KnqKQThpyqPQ87HJQd68T(n~(;vHE%NOJ4dq`RC8dGfsWkA!l?R zmg~dwu9iRVfzkGN(N)ekt#x%4_D`2-ow`t>73fF0h=Ro? zAAi#fwVi_zwpB-5^{tn$`;4&vsja=_0M2+-q&fCq2fIqKOXqe~Nu&eP8g2e#WoL}3 z88Hw5LJ)673%G9pKMWgv0kGxUA6!_5`v6eeMIaC^`1+{`f?ZXAEaHd%MT#GLWdBhL zdj!V1@`s98?brd`Ujzc;Lg}B21h8@8k5U4_zZQXi(g?!FK|g847V-bA5sZNS)O&2m z4E$LN7l!@R32Z{)XDKKM8*A@$)d7RFur|dI;|A-z6!w1rXYA27rksE?7ABlqW6!6C lMq>wlmk-3JZsBT*8>7wr2Z?dm95oOW422N0u}Q1Q{683xFn9m} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json index 46aa1a67a6f..6959fdcac73 100644 --- a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Star20 (3).pdf", + "filename" : "StarLarge.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Star20 (3).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Star20 (3).pdf deleted file mode 100644 index 11a9fc5e3bbf4d9da0b793bba6c66c1eaeee39a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19311 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 ZSRR`@cM)jxIUiy$SPVo~R>xTPe*glh9_9c5 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/Contents.json index da87fd09d2b..ae984eef381 100644 --- a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "star_18 (3).pdf", + "filename" : "StarMedium.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/StarMedium.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMedium.imageset/StarMedium.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ef053eb42986391cdec0cc5d5fae04711f38c9ad GIT binary patch literal 6974 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 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json new file mode 100644 index 00000000000..02b779594fd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StarOutline.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/StarOutline.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarMediumOutline.imageset/StarOutline.pdf new file mode 100644 index 0000000000000000000000000000000000000000..22b2d4a5d3013c36e8e3305c3dab2e5e654262f3 GIT binary patch literal 16644 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 SvBS*%iIoLFPA)1h_J0AE`J<%( literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/Contents.json index e283fcf4161..398cc928e1a 100644 --- a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "star_16 (3).pdf", + "filename" : "StarSmall.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/StarSmall.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarSmall.imageset/StarSmall.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a34ff4097f77d1c1d257b3fde757096df7253c16 GIT binary patch literal 6946 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 b@LjFgo4STb$2~m(adW}IxC{(e6~zA+RY3HB diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json new file mode 100644 index 00000000000..f6d2b394daa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StarTransaction.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/StarTransaction.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStar.imageset/StarTransaction.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4836b64fc5a13961147680ffe75e17d02645bb9e GIT binary patch literal 7031 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json new file mode 100644 index 00000000000..8b71ceb5539 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "StarTransactionOutline.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/StarTransactionOutline.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/TransactionStarOutline.imageset/StarTransactionOutline.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bd3a7e4f21b28186fbfaf539308ebe01419e862a GIT binary patch literal 17106 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_activate.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_activate.tgs new file mode 100644 index 0000000000000000000000000000000000000000..61f1538549f2aa06807dbf5c7d89f2b8f15ccef6 GIT binary patch literal 5659 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 B3rYY0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_appear.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_appear.tgs new file mode 100644 index 0000000000000000000000000000000000000000..3d4e32a67eb427151adbffaa08b57e2b6be52462 GIT binary patch literal 5796 zcmV;V7F+2biwFP!000021KnI*ZyZ;4{42&h_cZSJhu$_w?57PDaUK>ijDTxPmSR~_ zNM@7GLjOHgb#4!D4~G<`mA#H(nB??y-_LXF)Twh?e?8p(aC3O(m&2bA&klLbYd9>2 z7uSbp#Vvn(nzvK;>V@GSeV9A1v!Z+^lvf4F}B(-&XgUR{6jyLa!N<5xig z-+%wTp1iubd47xbzxj4KyuSL&^K1V7*VEUp-h8jVZ~uZ0Mtf@N_gAmppqZO%ef=Yz z`s?AEKd$V8T6x6_|ARl>8qq%5_{M+x9>cwV-SPVE;TZ;fGd}K~udQt@tUjN&l6pC7 z$7gHkSk7C@-9I#z(>HDnv2P<9Lu&Vi^wcS}hlcXh7Gkj$q8h-3bhk{r<=sOIkEYa; zpW5H9IcwXEreRsdFERTy#;`by;Px$hSe$)$b^0(*0&}>~L8)^OK8-GXSsnTaF@1(U zc9RHWfkH^uMRlLMi#TQx%9us&PwIlZHP$%f^GCINS7Y~_?Vi&;al1p0K76e5a{93u z(}Mrb4Qd(xZ-zC7HyMD30o1<4x_Ed0^eyB6NBStdVf?tz&1MB?eHs6MN-vMiWV3>f z)^YxSh92T+5^3q>=jdUxmEO>LIQ779@`VNb_B{>gt?lB=t9Q>0z6sZdAFct=uWrp` z-WZ=3Pf0{n~q9jty`f3LDK~ZD_RQHHF3t0k`N1{6US@YDmj5!$JDSw!S+&dvW#p z=K1@xqu$~NK`}o(yT0b-$mPfv=hoNGsr&rU)C$fw25?NcvvJ(V8Zg<0D;bA4@k%V1 zWzdzXxg8Ta^t#|eeQa`Z^vXZ7ai+S>gt^K3BVgLP$FIQnoFj4 z#ueB@(CqwLGAGwo-`mH|<;)Dj!?QVJtzbhPD;6(K%VQ_F?tyFj&~9P$4fEkFau~Z6 zbC#XOsw)XTC*hu2Fm>&g$6P!1*u!dljQ8hAD9MkFJT7f5j~Q5cXuyH?8O^Z#YDUyG z%*<~}Ty=b~k~Y3@Hag;v(Z;LWmtS}w#mn!vZz@wqza5wU;q|K@52xwfpUy9i(iO*UA)4OVJUAbq6DPNJ zeJYioV!6XGoFY$`)oYrbB9g)uYt=zt9q}04&fT@lqYzvpV&)0$i2(&yF&29mBX@;& zwP@(x)(rmR_38Wt{IWZ;Jy8vDLHGkm zR|uzj%G%LrUsGw*wU(+DO7~$Uxlk>0EE$&*-CF4R>{!;iX01&C7;%Yl?m9;Z46L3P z0>;_|o8}aKU8AQr!*4y&tOYPf-wjtiAQw;oI$E6$I1s5fJW~J>cEv~F51VNuo_2*j zqUVN{jRwaFXp#sGX{~ZwM;|hGQJ|f>?&^dz3>drZ+vp#E0X^ZX%tv9&ITBR>F|Cg) z9;A#$5Y@5=R)Rwv1MR5~xJ=w_5L$_R@FL&D-cZOo@KSu|Q{O_U+!*`jr3 zoa`4Pk*zvk9`?iK`9?`XP8-{h! zgcHsPw=z}izfqE=rm&JV(L{KtySj4Q=RS^Q%U%~kl?^3Y437b9SZ05$^r~Imv~D&8 zf~V6=hiin%mxe`)GS9)Gz_&1yEOQ)g8Oyw89&&`8KHFUm+>|)u6YysvR+W2=Z3!|^ z44UK>25Pg)*ccl1HVcn4TIt*qG0Z<=!=FaQHWM!Ho(X$@XTtF^u*q<7yDY^_7lfeq zo$*FAUuWydf%KDn+H5ZbLr>c_Fy3AlP*Gnm*==kew%h)g-3DJb%f$oJDLl7bMyTc_ z1VKbvg0T$~AXYT$-tNIKA*u>(TMA~oxf;9`Q6<`LF0wH%=mPkcs?Sw+!Ksk6V)L{4 z7+6;l+5(4-wbuxH>!Mh}ycpS7ke>tI`?T3^*Co2LVd2H@0=T0|gXv717f|M+yE^GI zsUM%`f}xw#FlGR8(Ujq3oDdntE$5C4l~F^$(nf<{*e)lYxX|q*ZTBhEo5M!r4GT;& zz2Hi1K#1>Zrnv>5XBZB2;jpSQ!*B*yp|kb_ic9PSkmAn5Gi^Nex_$JE zFlU~6#PDH~y-hHU)v-WQX2L|PFtsD;^N>l?M8<(-`{S!(gK8X zpJO}}dLh7Dyg!57Isg~eG@-E*>^+?pH+}vzZDTFq?8X-wT{~HS6G-a@G`ya3r}vc&-ze{NH`8Az93m7+GA@8t{h{+Bz6%UrlLn<^PS+5SUTp=A~v7 zhq`UxQb*%FKxGoFr84&zw<`?Dd)h|qVAaaIVB54!i2H_#P!C+-xpj0j` zWjpb3D;4A-OVH;^GZg}&LSi01zuIx>r}2=aAaRB+2~|CZ9c%feWRY?wi@*q-Afb0Z z9i-->F^y@2#K$1GvSbUoY?1BvkUj~r@*S1Jc>HeUPZ43f1+#m)73dL4eSmXS07ibQ z7?ViQ7oM^yf}nbVGXq2Jdv8@6fhpQ{gKN;CxiIymKyQalu=H>lnj+>3@MC-^WWtbe z5&|^=c7TJ>ff=4|u?aFj%!ZKHbZk9)10@qm6%o^9xOq=F0=#U(k@oeTa8wWw+2;{@ z`;%;xKEG_#m^DN=*euZbJhBmBnoi$a-0EExC<78L$OCf0LoEQy5t1=2i0E?hSaLr! z0E{i8S05Oijj_S~Fuulil_*4N#Hu2Qazp&aCINJ?B&Jd`xLd%TyIIv*K~bk-0&zZL*B=Td)$7LQ1({_{P($@6J#OVnr z^r)mZP-u5Ya1sbxc~D!c3+G>-s;$AUF=4IIoceCS_e@b{nhzZ!7Q5;wgOQbpTg*Bi zwu(FCY1ojmh)D$FK%}k|S&akX?Dbq0ai@Viq`O%KFOR{_J<*S6bBKQ7vgQ{~nx7@1 z5E5*fpC6iECi{}+*EY?M^o4Qjke&a|njgG8$y^;F30WNu3zyH-T{0s;IRGKsP%=ph zkcXLLg%H$wIv^WhOu=3&QLy#60KC#I93eo5#76!AxReN0o+O!15OuM39XE>m6@qtO z8r*;PpI3K;q0nc}2ZG4WixW-29%o{a>^Vsy!?j$d18`Bf#8OeX4ECXPMQP!pZaFJL zOkOfvaet$3+3<;;4$k3jsBE_@B~jyyA8W|oVzL2JfeI&XZgcEg8Ilpnc3=?{q~?&% zu#18^t9jZE3II_~I9AQU(XQEO3~d*`K1EBl5Vy~v*wducT*$6k&L1VE!oSA4s|t~0 zMQpTy9@M^rRRkjR1g1H97;qfNXky8lc%CXq7R##vy zb8DDhBub_c+c8?9ia^Ge#5}wSDhz48DvO z+v-0^ukMN2Wfrs3&mmMiSI0 z1>R&+Cg-TqOv(qNvkyx7UJF;o(v@|jhVtJ{%%||P#4u6|j=_!{TdXz=-yW610~}!u z&8F|*+Y-mU+KghyiKm%o*(QYsc#fOC;>lk)UP*NN25Z|sK5_8Fi^CI=06*}CcYp%j zuov~2W+<%o=E2aXno0aUaY&zARV_GR?frfIzmr=4!@327xZ(N!2F2K9F2w2R(pI`X z=JwU~)%UNSzq$SQfBw(w7dQX(yZDDMe+zkvFR$Oex&0O9DJ~RD;OI$pS$u*io8 z?Yp3bdq_Fke;^3E3&#tqc9}VilM`X)f@-uMxk``|#F(JN9(-<8OPI&AO`GBI8p zC`4ps27H*Iq}s zIx-d14ASJb&~3`5{M5dSCY-VWE`%M;r23+DfGD{8Ig0 zwJ$Y(N$g}}eUnUq=L}0Njt{Jkmv$c%1+%hVG>r^XFyC!bjB=&MARs5X06yb_d| zG%|Lkt>jIpC3wkS#Uwl``mt~h$rNSB7z>yL3>AUufkiP?6pI3P=7q7aL%I9%#OO8R_FJ(vEJ^a32h3{el2hHy^>D0|~W15mXOAFNrU+z+anNfzzPy2ox-R zlxed-27w~k_g2D#zy8~h z{*#nAodZl$(JzB2+acNl&swQd6d4@DK*G=ze^NL%y?#1#p1g)UYL0gK3XRFgSseM3 zpe2fmjWL!buTMpf-I`M0Lx9c*u8r)a*09YpaD;L&otk!HX_3Y-!xpJEv&OjE^DgJ= zXlKl3MhlK4de&(xniB@aHbDtrqX|~d)~w>qGi>4&zI>TS7FAi^HZ*@+U_Jq#EkUg~ z)1bssG>z#aDQexyf5D<6i@)*~HhP>~Pnj;_89^9Tr%s_ZP#!TX86u-+Hf=C6=IZ(K zNmZ2!=2Dr4OIt#rFyh1RhoOXIjDw1x+a&T1ut7UNYg@itx(w98>s#v!D@)h7k zOiO+%l_XChqBG{J19-C4y*6ou8_Xy_!@TRrW3i;P4dI8W{A35qozeva9o7;0Et%m| z)3myH8jF|k1g{_LriQJiE<|QNdS>X$+&XrqSO&8;D*xK&()=%y0k|xIe-e!BnhDx< zc&juL8;u@dO@59zG2NK;xHv?xo+&{I z@q^P#e=HfWbR18!w;F;Z17=w2MNGT^2tF^#QsuTs;F^k6E*MktyoZ831*169kl+Ya z_~)&Z9+?em@ z!AVt0h;dZ=lg@fXna#>5G7}Q8fqr^TjDa@61JSF!jYB`*95^=X7__ZrppCEzdR?G?Q5V5F| z(T~>xsP8hHzDpdiplP=PUJ?aM&qlUhgqgy{y*QT6v+nkqS#lN^jnsgO;*y)It62A7 zTJeq`8I_vDVggzYRN3;J>hSi;3ln6uj(RQCr}#E|h#r(-cCqYWE-{#*wG9p>z zXHVj@N)voz^50tOdVRzV4W94+GF)I761GGoRr2OXcG+iU&8ot>(Cob&vfnCcO>4tU zT<7Z!U@7C(9iUohkrf=xG&`qqLSjkYYF?cWMrfjCTD{OGvL25(=ygH~p-$aaD@mY$ zH$t6N4Y-ly6I4*)Zj1-F7>j3)5uzVuR;;7I5-52v>cF_sqFdyRwa`65Gp z)4lhyP2Wj2reJ@LI4Vmsc0&vYrI`*crSR}nKEyXv=OTm3S3xbGz4~3M<5Ly zr18(oA33}b*BP#f;Gar@vXy4c5Rr9z8v}5b1F?@?*$;dt4W;W%tzw@{l5e)=Rb3WK zB=8P~P!=qy39yeo$d6=s?FB;@$tQUfO&}h!74t!II-@oYf}8D|Jw#Z+hqR7V^P&PH zo3(u~r!Qk_n^y1DM70e%viYp$?R@d{EmB`bxRDkiKVJt3Hgc|tAG&*IQXurvG|-Dk zF_gufum!~CPRDfBx<{^7UhPcr@g{@#D}Z*RX!|M)LIV?Lt#beeB3 zk|-NW+^6JMw{^>XIj|E(Pckv$! i_}2cVFK_?S7ZqLp8w}5YeReMl{r>>s6Iwr|{O+M+C; zDN-RRCyq=1{hp^Avx`~oQj{fX*UA=);KTV((2WLR9B{(N@L&%dma&o7_Lt8af^JPj>3&FJ*}@`*=AH`j8* zk9h8v#cTdKl_!5hJ}#!wBs;iRNAKNWPTmeo?(Z>jhhL}Pv~;WKzbwvq;U98q7}_sN zixNGv1Ah3>)%qSgwBL4*8H{FpyLeiv-)-+;yYd~?40Zhmrnu1FZsfYAoxG*guSDWEFHc{eX%n+a7e8F1 z3D0j;&X?Nel5q2KF*WL{t)v#uSeBEa{ar>BvrrUr;Y2Di-BQOoEG-+Wd*(!N{OsA% zCI>Mry=OMrj&4|nI97hcf21To7Jg%ME^Tz9_ zX=t^(xn0NuJ>EpxoxE2mkkwo1KmE0gOBcui62JIA`S;x`No+mg`s&BC9WTG7Xg4~9 z+1>@_z@G{_wcoI~o~4+s=-WEUgqb@XBTcB*&x)p+6f#>uMDXCtk9XdGb9(2JNaUOA z)61I|SJ$r=Z%^8#FD}l1T&(1FS56Ov;GLS9v3M`_kxyM+2^hIP@ANbHM(~KWD`Z;f zky$J!zC*@=@swBVA&7KXtF$%#!w3pJ^G|w zs5(4SN?!G!WiUdBcu%ce?x-mXBab-{6+XLlYCt8{<;D$38H69ppuK78nQ6#<9Z}-o z`MOp{o5xt}kVcz%m&462OPP)1+mT*)Kk`q0)u*iqV?xgjd?sWTHamH3V)wR@AEnpZ zWXXT%_-Kcguw)m<6H@rh23kfh?19@oNg&wiYh~WP=$Fhir!QJs?;)6=G6IB=r47;^ zyuMU{6lCr(Hf1S8g``j$YwTz(Utm)iv&S1y@8o@>o!jd-XK!a@elG-5ZYlWGx|-i7 z5SdqN!(gOQA*2L>Lzc?AW43uf8-%h3qDz#T8$SUEFfPEQ!q}C-g#-0(F7!8 zu<>lQfV(}vI+IrbL`ymksYHyu^sp#qzlJ~;G9CZ`tWJY@uKEx!Q#M%2Wl^;fH)|9>S z8i>nsiZa$zwYHVa0A;H9*5h&>AlTGtco31x!XG(*T9|>|@R&2wztNoNkpN4Hc>{L< zN~&Z$6vX7iyJ(?M69rPu3zBS&QOrxcZJ}KDnRM|F#*nnCUpv429I5;+C z9h>SFz+$YA5d{?^N9D*7;?_w+Keavp*bvc6Z5n~h;MigZ1Q&ry{v3dUNjw@@?zgtc zYoPSb0dn#HbJmNCZRRY<3+h2J!9cen`33xQHex~#^?IkFOU<^uHS5dsi;I2Qvij0} zn91qum*-eh4I?G!*@n0uaqBQp1*^9+`Zs1+wrZOY06=PtYWM_rFpG}zhFfPg9lK2oV2{X$y z3lVYaOxL0StMprsTyxhFLJeJ8u!NdzRNh;gyignr0!mfY5+qJCCFX-xS?oyzZ7FIy zvow2;YSj=X4O)%|v6~gar6xKt&!Dp_*X@PhtS7;{#EAt3Rfox09r4Jy|A6_gTK#D)K6)oRb zpeH%fZt{_^A(l4wXP3AC`_s37`Gb4&=NI{Rp961yd3|+x`>}aj=%eq;oO(rNwMbf~*4a#3i+G=7B6w_c_g$&RkC3>hzJ`6xi!6LKB;%1BpM0wwRy zM!#j=9JuTdo7F%VY3vI@Q=jkJin%TOrXvx$zA-B~Da#AUL(9%pC<)OCG0auDs#|0fJQXQH> ztS!!OurAdqC<$AJ1==?R5U#FAgx%}Yt6}2mW&YzgZ(e-o{_Eq8haXMAm_xg@kd1w( zm_?8_tVCQB|0dj^Y1N4xXt#`UOJsj9Tq>$tWgVei6yP}S8h!uz*MGnK@_%ms?fOrj z!|40P>GxX~>QB_mh|B17GjfrBBoGHwJ-wL~o;eJ;xx>)!&ac0}ID5CR(7@kq;-N9X z*{w3_d|*D<%Vi6iCoET=--Tsp^}TIpNsZX~HZyGB)E{7dxm}$G>1}Dlj+~|;jZ-ZQ z9kPV0AKHzptIO5pU$c5+QfMj0q~=hO=QXtf(?y%70=BQBy7vjw^{KYoQ-_`Tbv1S} z#lhcc;;mC2y|Bgbwz2ZQ$tK&uZ3^inX!=pIp%I_q3dQ51Zb-E@~faxl=kl)M4oN z7Z>NRZ`7-9*H}cZ4eyjkV*L%b8Gp{8?kopeUqyb}-EJ@e!B zim$G(-n`!4Cx*uc6#7B5fDjPX7Fg)rf>~tdzay%C>!@=3Kov>KbL_-?SS&U)BKspt zeFk{)`@qxExjMQKWZ1S|?R!?YpM){gaKzPb9amvLxXSdCE<^U>=_%99F&di0k7V`R z2UXk;s4&;}#_7^+6*s}vl;Vh~-#Vt!J}_l1)bv2xSOH&Ub%%5r81oSgS&y*#t-~ts zg{rKrvLd4cq59ETuAYb~`UtGg!K`5RVpk~K8p$;{ZU`{(I0k-Bn5y?d>h{&*{*C^> zssZAi24Kyn-Oc?H??-!O*xBaojaPTX_G-P5{LS_Gt0zSD%|^>lORl*^b}-G(9o#NE zchElRTh9Oa=Hf!;OEW|F-NM{4eV4(}J>gw}7`R^8{p|%eyM9NAL-)5A=|I|y9(gOG(^$s!Cj?yD5xAY>5^ zqK`y#2(pOb5c)`Ln?q|F2+(~EBkmAnks3M$nSO7js2yLVeU&1+L%>Cj9bfEE7I`aQ zFlE(NbqpAKq2AsJD%HgxlZvlag?usS$<;AJYCtsglpItV+Yn$%+{;IpHQ{o0A ztSi~NMQKX=>rGEkt|HmIGltpb4G(kr_(0dhZJU%sze(B5eYlR>AdcH0j@uv(a2v#m zh2}8@xc|_{fkAU>-_G7KbjAdo;$&l}iLPy&<3Pf#e6coI1oqCcR1I2#2P1b3q9j$k zQOCjHhdxe`L|N z$52~pkdn3i{e(k%woW)HjJ0H2C?i~-IB2QTU#`Rt+KO?5kTOO*E&u>ceG&0mFAbii zz$b?AphHGc;Z~vz17*?uq?TYqVZ`%RgJJQhR5iJ>J#km8YwkjKD~1p@VT{@faEIrf znM6l%+cxlefjP)rfeaCZ+*nTT$-u0@!w^@jMFn0Rid!LSFWF=U=5i9L#z$6Fjr?Lxxzq;t*n~cExq%M4SAI?yAHKz?Y~Im_{~T zV_?vXcu{i;tOlC_Q<_|a9!u2VJ3cT5K*xPvqV=uGkD>1%LgB>P7IYFL_hoc*KQT#W zh*arAIvArI+yU=x3-JSmLS77-YP5)66J-dj>L|g@t2GOqc&Gl@;BsPn>6>>d8YC+E zBKxh9EndRqB?uzt`u=eewl4-yj^H{#%K;LyD+}a_{t_w}^??CF%dfZM?~2|~Q*}8Q z6LKmKF4gB8yQTZ}K~T@2%2{i-%qjxyVN)Hm6wR$dWfAN6H%CPGfs$OMn`GB?$% zYRw+AVlI9 zC|T0Ua*dD19e#z6XhrADAw1g4nmLUV^1=aU4C%4nmD;igCUO#l5cP|X29LCY2V>OI z`tgX!Pt8n6_Kj(%jA!>)@0vF6>exbkH9lIsa#*7y>Mk1lZg_NNN)(T7ioazbGlH|Tt1KXY-|re<2N9vRyaUw7K<@TY`Ne=`&t>k zuzKGcnRCHoKKIvR7KDf9YU||K5sgvJ0}~j#*5Mh~*3WVli!xb%y~n`Oa4iPWC`S0m z*vq(3Y&*0dF|u?ATiN8&0hjjR3A!B3qOL>f*i{^~78;BhKU~d>c+_!QgReZTV#02g zOFTKH-4%4J1D9@WnT(u?R$M{X!m6GXs8l0KM*rJxA$79L~`aW{i_+JHES z2#87ypg6;Lpo>B*ZJmoVfY89Y4$^68kj@>*QHmO*6Saj22%NK6#e4?-ij^jeQeiZ9 zRYkhQHOK>d9quArB&<#n+;Cl>N_n{2<$H{WImTDCy@VQQY3`TEu{?H_%m(PNL9{wsT$lQ zjm8#&JcgFe?(0cl(>g9=zB>XXKVX$|CHBa*fYiFy8nkFzFOaFK4@C51jZXPA!%AFm^UN^B69kwUe|D$(W%h`_U8@Im9OM zN60)Yjso$sp1!H0gnk-Dca;bQpzrc}FrzvZ?b1t7AkU~u$Rq{XiZ>S5Sm?u?CfH0h}mB=VCvYr<(sNTfP#P`tYVTYJBxi%AU zO*Kg_ZzT>rOAo_tZo{pdi3t5u#R1VJVGOUDb`-xblk(glD9G_b-o)J+m6b_B&)QI8 zFuf?fQs+41Bx#4nGLFXv7gu&^R4X^pUXP;w8^mdwV9LM^^j6`Qg+6mD0i|Cr%W$o!-Q<1-1@J6&P=rbuP z4bgkbN(1PzS~f!~q}C@VIVytaqn2fAY^1)_^Ney-lG&UnfN7TkXxX-N>Tm6T^pd*7u+W(;n``J9~64sX%?JY)1eA^g{m|Mfkt$JHSh84q}VYZ z36?k}kJ;vA1!OZmFk_Rpk|bsu3a)$gVCBr3%%_f%;%2su3Qzu}PAvHSKi%t>um1SY zul@1Ff}?-`(ZB!b-+%P)Kl=9{{rivp{YU@)qksRyl;ICL6lSeHzTZhLpW>XBzrDQr z_HY0FMgH#9BhCUIdTh!g9h&m@pTGb1t2cl6%Qvr&n-%^gHY=RG{eQd8Rwu)X004`a B3rYY0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_effect.tgs new file mode 100644 index 0000000000000000000000000000000000000000..0b87a225a4f0cb162cc29de04ca74261253d11dc GIT binary patch literal 8420 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 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_select.tgs b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_select.tgs new file mode 100644 index 0000000000000000000000000000000000000000..61f1538549f2aa06807dbf5c7d89f2b8f15ccef6 GIT binary patch literal 5659 zcmV+$7Ubz4iwFP!000021MOXHa~!vk{wu2eJUfN=7kzXtcIuM5O3qH5>r|{O+M+C; zDN-RRCyq=1{hp^Avx`~oQj{fX*UA=);KTV((2WLR9B{(N@L&%dma&o7_Lt8af^JPj>3&FJ*}@`*=AH`j8* zk9h8v#cTdKl_!5hJ}#!wBs;iRNAKNWPTmeo?(Z>jhhL}Pv~;WKzbwvq;U98q7}_sN zixNGv1Ah3>)%qSgwBL4*8H{FpyLeiv-)-+;yYd~?40Zhmrnu1FZsfYAoxG*guSDWEFHc{eX%n+a7e8F1 z3D0j;&X?Nel5q2KF*WL{t)v#uSeBEa{ar>BvrrUr;Y2Di-BQOoEG-+Wd*(!N{OsA% zCI>Mry=OMrj&4|nI97hcf21To7Jg%ME^Tz9_ zX=t^(xn0NuJ>EpxoxE2mkkwo1KmE0gOBcui62JIA`S;x`No+mg`s&BC9WTG7Xg4~9 z+1>@_z@G{_wcoI~o~4+s=-WEUgqb@XBTcB*&x)p+6f#>uMDXCtk9XdGb9(2JNaUOA z)61I|SJ$r=Z%^8#FD}l1T&(1FS56Ov;GLS9v3M`_kxyM+2^hIP@ANbHM(~KWD`Z;f zky$J!zC*@=@swBVA&7KXtF$%#!w3pJ^G|w zs5(4SN?!G!WiUdBcu%ce?x-mXBab-{6+XLlYCt8{<;D$38H69ppuK78nQ6#<9Z}-o z`MOp{o5xt}kVcz%m&462OPP)1+mT*)Kk`q0)u*iqV?xgjd?sWTHamH3V)wR@AEnpZ zWXXT%_-Kcguw)m<6H@rh23kfh?19@oNg&wiYh~WP=$Fhir!QJs?;)6=G6IB=r47;^ zyuMU{6lCr(Hf1S8g``j$YwTz(Utm)iv&S1y@8o@>o!jd-XK!a@elG-5ZYlWGx|-i7 z5SdqN!(gOQA*2L>Lzc?AW43uf8-%h3qDz#T8$SUEFfPEQ!q}C-g#-0(F7!8 zu<>lQfV(}vI+IrbL`ymksYHyu^sp#qzlJ~;G9CZ`tWJY@uKEx!Q#M%2Wl^;fH)|9>S z8i>nsiZa$zwYHVa0A;H9*5h&>AlTGtco31x!XG(*T9|>|@R&2wztNoNkpN4Hc>{L< zN~&Z$6vX7iyJ(?M69rPu3zBS&QOrxcZJ}KDnRM|F#*nnCUpv429I5;+C z9h>SFz+$YA5d{?^N9D*7;?_w+Keavp*bvc6Z5n~h;MigZ1Q&ry{v3dUNjw@@?zgtc zYoPSb0dn#HbJmNCZRRY<3+h2J!9cen`33xQHex~#^?IkFOU<^uHS5dsi;I2Qvij0} zn91qum*-eh4I?G!*@n0uaqBQp1*^9+`Zs1+wrZOY06=PtYWM_rFpG}zhFfPg9lK2oV2{X$y z3lVYaOxL0StMprsTyxhFLJeJ8u!NdzRNh;gyignr0!mfY5+qJCCFX-xS?oyzZ7FIy zvow2;YSj=X4O)%|v6~gar6xKt&!Dp_*X@PhtS7;{#EAt3Rfox09r4Jy|A6_gTK#D)K6)oRb zpeH%fZt{_^A(l4wXP3AC`_s37`Gb4&=NI{Rp961yd3|+x`>}aj=%eq;oO(rNwMbf~*4a#3i+G=7B6w_c_g$&RkC3>hzJ`6xi!6LKB;%1BpM0wwRy zM!#j=9JuTdo7F%VY3vI@Q=jkJin%TOrXvx$zA-B~Da#AUL(9%pC<)OCG0auDs#|0fJQXQH> ztS!!OurAdqC<$AJ1==?R5U#FAgx%}Yt6}2mW&YzgZ(e-o{_Eq8haXMAm_xg@kd1w( zm_?8_tVCQB|0dj^Y1N4xXt#`UOJsj9Tq>$tWgVei6yP}S8h!uz*MGnK@_%ms?fOrj z!|40P>GxX~>QB_mh|B17GjfrBBoGHwJ-wL~o;eJ;xx>)!&ac0}ID5CR(7@kq;-N9X z*{w3_d|*D<%Vi6iCoET=--Tsp^}TIpNsZX~HZyGB)E{7dxm}$G>1}Dlj+~|;jZ-ZQ z9kPV0AKHzptIO5pU$c5+QfMj0q~=hO=QXtf(?y%70=BQBy7vjw^{KYoQ-_`Tbv1S} z#lhcc;;mC2y|Bgbwz2ZQ$tK&uZ3^inX!=pIp%I_q3dQ51Zb-E@~faxl=kl)M4oN z7Z>NRZ`7-9*H}cZ4eyjkV*L%b8Gp{8?kopeUqyb}-EJ@e!B zim$G(-n`!4Cx*uc6#7B5fDjPX7Fg)rf>~tdzay%C>!@=3Kov>KbL_-?SS&U)BKspt zeFk{)`@qxExjMQKWZ1S|?R!?YpM){gaKzPb9amvLxXSdCE<^U>=_%99F&di0k7V`R z2UXk;s4&;}#_7^+6*s}vl;Vh~-#Vt!J}_l1)bv2xSOH&Ub%%5r81oSgS&y*#t-~ts zg{rKrvLd4cq59ETuAYb~`UtGg!K`5RVpk~K8p$;{ZU`{(I0k-Bn5y?d>h{&*{*C^> zssZAi24Kyn-Oc?H??-!O*xBaojaPTX_G-P5{LS_Gt0zSD%|^>lORl*^b}-G(9o#NE zchElRTh9Oa=Hf!;OEW|F-NM{4eV4(}J>gw}7`R^8{p|%eyM9NAL-)5A=|I|y9(gOG(^$s!Cj?yD5xAY>5^ zqK`y#2(pOb5c)`Ln?q|F2+(~EBkmAnks3M$nSO7js2yLVeU&1+L%>Cj9bfEE7I`aQ zFlE(NbqpAKq2AsJD%HgxlZvlag?usS$<;AJYCtsglpItV+Yn$%+{;IpHQ{o0A ztSi~NMQKX=>rGEkt|HmIGltpb4G(kr_(0dhZJU%sze(B5eYlR>AdcH0j@uv(a2v#m zh2}8@xc|_{fkAU>-_G7KbjAdo;$&l}iLPy&<3Pf#e6coI1oqCcR1I2#2P1b3q9j$k zQOCjHhdxe`L|N z$52~pkdn3i{e(k%woW)HjJ0H2C?i~-IB2QTU#`Rt+KO?5kTOO*E&u>ceG&0mFAbii zz$b?AphHGc;Z~vz17*?uq?TYqVZ`%RgJJQhR5iJ>J#km8YwkjKD~1p@VT{@faEIrf znM6l%+cxlefjP)rfeaCZ+*nTT$-u0@!w^@jMFn0Rid!LSFWF=U=5i9L#z$6Fjr?Lxxzq;t*n~cExq%M4SAI?yAHKz?Y~Im_{~T zV_?vXcu{i;tOlC_Q<_|a9!u2VJ3cT5K*xPvqV=uGkD>1%LgB>P7IYFL_hoc*KQT#W zh*arAIvArI+yU=x3-JSmLS77-YP5)66J-dj>L|g@t2GOqc&Gl@;BsPn>6>>d8YC+E zBKxh9EndRqB?uzt`u=eewl4-yj^H{#%K;LyD+}a_{t_w}^??CF%dfZM?~2|~Q*}8Q z6LKmKF4gB8yQTZ}K~T@2%2{i-%qjxyVN)Hm6wR$dWfAN6H%CPGfs$OMn`GB?$% zYRw+AVlI9 zC|T0Ua*dD19e#z6XhrADAw1g4nmLUV^1=aU4C%4nmD;igCUO#l5cP|X29LCY2V>OI z`tgX!Pt8n6_Kj(%jA!>)@0vF6>exbkH9lIsa#*7y>Mk1lZg_NNN)(T7ioazbGlH|Tt1KXY-|re<2N9vRyaUw7K<@TY`Ne=`&t>k zuzKGcnRCHoKKIvR7KDf9YU||K5sgvJ0}~j#*5Mh~*3WVli!xb%y~n`Oa4iPWC`S0m z*vq(3Y&*0dF|u?ATiN8&0hjjR3A!B3qOL>f*i{^~78;BhKU~d>c+_!QgReZTV#02g zOFTKH-4%4J1D9@WnT(u?R$M{X!m6GXs8l0KM*rJxA$79L~`aW{i_+JHES z2#87ypg6;Lpo>B*ZJmoVfY89Y4$^68kj@>*QHmO*6Saj22%NK6#e4?-ij^jeQeiZ9 zRYkhQHOK>d9quArB&<#n+;Cl>N_n{2<$H{WImTDCy@VQQY3`TEu{?H_%m(PNL9{wsT$lQ zjm8#&JcgFe?(0cl(>g9=zB>XXKVX$|CHBa*fYiFy8nkFzFOaFK4@C51jZXPA!%AFm^UN^B69kwUe|D$(W%h`_U8@Im9OM zN60)Yjso$sp1!H0gnk-Dca;bQpzrc}FrzvZ?b1t7AkU~u$Rq{XiZ>S5Sm?u?CfH0h}mB=VCvYr<(sNTfP#P`tYVTYJBxi%AU zO*Kg_ZzT>rOAo_tZo{pdi3t5u#R1VJVGOUDb`-xblk(glD9G_b-o)J+m6b_B&)QI8 zFuf?fQs+41Bx#4nGLFXv7gu&^R4X^pUXP;w8^mdwV9LM^^j6`Qg+6mD0i|Cr%W$o!-Q<1-1@J6&P=rbuP z4bgkbN(1PzS~f!~q}C@VIVytaqn2fAY^1)_^Ney-lG&UnfN7TkXxX-N>Tm6T^pd*7u+W(;n``J9~64sX%?JY)1eA^g{m|Mfkt$JHSh84q}VYZ z36?k}kJ;vA1!OZmFk_Rpk|bsu3a)$gVCBr3%%_f%;%2su3Qzu}PAvHSKi%t>um1SY zul@1Ff}?-`(ZB!b-+%P)Kl=9{{rivp{YU@)qksRyl;ICL6lSeHzTZhLpW>XBzrDQr z_HY0FMgH#9BhCUIdTh!g9h&m@pTGb1t2cl6%Qvr&n-%^gHY=RG{eQd8Rwu)X004`a B3rYY0 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_static_icon.webp b/submodules/TelegramUI/Resources/Animations/star_up/star_reaction_static_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..9c9b83471d98f5bdd3499ba5879b13875d8c092c GIT binary patch literal 2850 zcmV+-3*GcmNk&E*3jhFDMM6+kP&iBu3jhEwN5Byf2?cE%IZFGx&!6zZ5Yhh$;9Jo{ zmrV?a0++B8Lg@bl;K`l{ z7&!qDzz+bt{0&ATzz!A)(SuB8F{!b7>}va;MwaxK4CUejMZ?U@Oc!zTVdj|&=yWmK zXfI-Kf*B)bI^0mzRd(6ZV0X1Eo9Gt}w0j}!rcIWSl zlg|19wv)=N?B42~lAUeae$D$jl&Ebxq(@h|Dz=@BZQHi(nU!tZHrBT7Wo+9{y8BO8 z)dUa%JmLPvxAZT&2elk4K!wlr!M%iy(|5<)dJ8kYwb1*hYh}9eo?kTO%>TP5cAq{n z&+l01ecqk28HtPTmcRT1jn02=oVwfIHA*+70}`!d*cn|PU7RbDZBYSqg8Y6@%ubVNtrVPigF=|Q zul-v-U14~*w^mj9bP6{pu?F_@0*@XIHfgVjio7&&)l}X7k})crb?mi~O4=Q%iy+}w zOT3MsYX{)?y+WG%c6}(FCXqe-%Sz8_x>?9d*vIpT1fKyl*6FT8IqTdyxO|FkdBs+$ zIqMH1IvlR^A?7E2-Ur|iJD7%JcM8uuaPxbT=?RlZ4%sTBjIV_xn7u-phjzUnnihda4q2*RHe+h>2^MYP-ZJUZt)JD(r^EG%_rF$kFF=Yl{*T-&F>o!H6zDV zr+ISRQTs@u7;~sjg@0p(1Ag<4c;3Pxz5N>nxXG%F&$Tk{-U%Ww@Ev2j@>V%bEUMhm9 z@Z+ciX+ToDB0s9l$G&6BXonEx#Di$VNj$p1#|U*lNxlbH^LU=<{!*cb(SOk>cj4lW!>m^%Tl^HMpq zPAFkd#}M?;uX)&Gr|s0xHZ^S&u|~*B0YivD@aQ35`xp<%n6{e7R4f`ZZEyR;F#eau zoHQ0?J}>bFS_vEPA(_uv(spLf~GClubd;F>R=(E0yFqzWEivmoIwF+b_^ zu09{aWD_PV^1Y0C0yKpNY$`43_y0OL`Q4cGMxa5j)eWS7RjNoK87u9prvL!OyfJ_J z^k>KX<t@0BqbS%G)|k)ZQ}r}r{p8z)o{XD&QM@`N#fOcVc9;W2 zZGjK%=LL(r25XzKWWhgM8h~P3d>s{DY53azOJ8i=V^DaCtEAD1+h%koRpcF zel;sCW*z#wd4x8wFtbyf<0*W zFJgt6Y~`EBDV(xxMfV!<&z@_QW$NCG*;xUGiNs*mQSkcbNh;);%utDk6)tGb)V+ly zC_LB7W9q376orKYTR7OShov2F5dwh{c}`6KQ0}8}b$z1ju4G{%R7&R4A{bfzYae7hSkaoj(Sw@+p!M$ zRaHNUznTmE+G}xOl{U-l&>#U?363ocU>(@PF^XeuTCKLM9ru*3w>9<6p;K=5ekW?* zYHW3;PiH5t(&cbg^Hl@13T(mwUk4c002bzm{W8^g%ZX3pWBu);?Q+wexKzsMLnMuc z4o94<;i=*ng@Gn8ED8%o!ElD*(FVS>$PLd++Dn8y-bSItp?|F;g`xo%;OoHm818$} zBz7=>g->AL)yfRdi#OO;o31sZe%2*AwpcjE*8xSCDVTka0RzXdfKuU6+wiiCS~V;$ zBB~=QAwcz_hgO4O;n+HS4-8nxuz+C)PizD(maS}^7ol=k*<@TEIw(sHY~dJSSXeM% z7|sAvaNy`)*RcJV-Sfg`p05e5votwd}tXQI$B=JxH?~mN_}Hi%j`VCw`68c6QESUv+$IbV18u? zVQwzs&KT($KMAbO=7u0VGn*}F-hQT)L|Y6R2djpz)@Zz?Fj%;YBV&7#Nh49x)`+P6 zWaF9Q8Tg1TS2mg take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak targetView, weak itemNode] value in - guard let self, let targetView, let itemNode else { - return - } - if value >= 3 { - return - } - - let _ = itemNode - - let rect = self.chatDisplayNode.view.convert(targetView.bounds, from: targetView).insetBy(dx: -8.0, dy: -8.0) - let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: self.presentationData.strings.Chat_TooltipAddTagLabel), location: .point(rect, .bottom), displayDuration: .manual, shouldDismissOnTouch: { _, _ in - return .dismiss(consume: false) - }) - self.present(tooltipScreen, in: .current) - - let _ = ApplicationSpecificNotice.incrementSavedMessageTagLabelSuggestion(accountManager: self.context.sharedContext.accountManager).startStandalone() - }) + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { + if !"".isEmpty { + self.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: self.chatDisplayNode.view)) + } } - }) + }, completion: {}) } else { controller.dismiss() } }) + } + } + } + + self.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: false) + self.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1) + } else { + let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction + + let currentReactions = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: self.context.account.peerId))?.reactions ?? [] + var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) + var removedReaction: MessageReaction.Reaction? + var isFirst = false + + if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) { + removedReaction = chosenReaction + updatedReactions.remove(at: index) + } else { + updatedReactions.append(chosenReaction) + isFirst = !currentReactions.contains(where: { $0.value == chosenReaction }) + } + + if message.areReactionsTags(accountPeerId: self.context.account.peerId) { + if removedReaction == nil, !topReactions.contains(where: { $0.reaction.rawValue == chosenReaction }) { + if !self.presentationInterfaceState.isPremium { + controller?.premiumReactionsSelected?() + return + } + } + } else { + if removedReaction == nil, case .custom = chosenReaction { + if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info { } else { - itemNode.awaitingAppliedReaction = (nil, { - controller?.dismiss() - }) + if !self.presentationInterfaceState.isPremium { + controller?.premiumReactionsSelected?() + return + } } } } - } - - let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in - switch reaction { - case let .builtin(value): - return .builtin(value) - case let .custom(fileId): - var customFile: TelegramMediaFile? - if case let .custom(customFileId, file) = chosenUpdatedReaction, fileId == customFileId { - customFile = file + + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + if item.message.id == message.id { + if removedReaction == nil && !updatedReactions.isEmpty { + itemNode.awaitingAppliedReaction = (chosenReaction, { [weak self, weak itemNode] in + guard let self, let controller = controller else { + return + } + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { + self.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller) + + var hideTargetButton: UIView? + if isFirst { + hideTargetButton = targetView.superview + } + + controller.dismissWithReaction(value: chosenReaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = self.chatDisplayNode.bounds + self.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, onHit: nil, completion: { [weak self, weak itemNode, weak targetView] in + guard let self, let itemNode, let targetView else { + return + } + + if self.chatLocation.peerId == self.context.account.peerId { + let _ = (ApplicationSpecificNotice.getSavedMessageTagLabelSuggestion(accountManager: self.context.sharedContext.accountManager) + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak targetView, weak itemNode] value in + guard let self, let targetView, let itemNode else { + return + } + if value >= 3 { + return + } + + let _ = itemNode + + let rect = self.chatDisplayNode.view.convert(targetView.bounds, from: targetView).insetBy(dx: -8.0, dy: -8.0) + let tooltipScreen = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: self.presentationData.strings.Chat_TooltipAddTagLabel), location: .point(rect, .bottom), displayDuration: .manual, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }) + self.present(tooltipScreen, in: .current) + + let _ = ApplicationSpecificNotice.incrementSavedMessageTagLabelSuggestion(accountManager: self.context.sharedContext.accountManager).startStandalone() + }) + } + }) + } else { + controller.dismiss() + } + }) + } else { + itemNode.awaitingAppliedReaction = (nil, { + controller?.dismiss() + }) + } + } } - return .custom(fileId: fileId, file: customFile) } + + let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in + switch reaction { + case let .builtin(value): + return .builtin(value) + case let .custom(fileId): + var customFile: TelegramMediaFile? + if case let .custom(customFileId, file) = chosenUpdatedReaction, fileId == customFileId { + customFile = file + } + return .custom(fileId: fileId, file: customFile) + case .stars: + return .stars + } + } + + let _ = updateMessageReactionsInteractively(account: self.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: isLarge, storeAsRecentlyUsed: true).startStandalone() } - - let _ = updateMessageReactionsInteractively(account: self.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: isLarge, storeAsRecentlyUsed: true).startStandalone() } self.forEachController({ controller in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift index ada64473b48..2450bcd1d27 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenViewOnceMediaMessage.swift @@ -177,6 +177,6 @@ extension ChatControllerImpl { contextController?.present(c, in: .current) } - let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .singleMessage(message.id))) + let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: .singleMessage(message.id))) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 5d89917c5cf..58693074561 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -163,7 +163,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u var botId = peer.id var botName = botName var botAddress = "" - var botVerified = false + var botVerified = peer.isVerified if case let .inline(bot) = source { isInline = true botId = bot.id @@ -195,7 +195,13 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u return } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let source: WebAppParameters.Source + if isInline { + source = .inline + } else { + source = url.isEmpty ? .generic : .simple + } + let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in presentImpl?(c, a) @@ -294,7 +300,9 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() openWebView() - }, showMore: nil) + }, showMore: nil, openTerms: { + + }) parentController.present(controller, in: .window(.root)) } }) @@ -312,38 +320,38 @@ public extension ChatControllerImpl { } static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void { - let activateSwitchInline = { - var chatController: ChatControllerImpl? - if let current = controller { - chatController = current - } else if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { - for controller in navigationController.viewControllers.reversed() { - if let controller = controller as? ChatControllerImpl { - chatController = controller - break - } + let activateSwitchInline: (EnginePeer?) -> Void = { selectedPeer in + var chatController: ChatControllerImpl? + if let current = controller { + chatController = current + } else if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { + for controller in navigationController.viewControllers.reversed() { + if let controller = controller as? ChatControllerImpl { + chatController = controller + break } } - if let chatController { - chatController.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil) - } } - - if let chatTypes { - let peerController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) - peerController.peerSelected = { [weak peerController] peer, _ in - completion() - peerController?.dismiss() - activateSwitchInline() - } - if let controller { - controller.push(peerController) - } else { - ((context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface)?.viewControllers.last as? ViewController)?.push(peerController) - } + if let chatController { + chatController.controllerInteraction?.activateSwitchInline(selectedPeer?.id ?? peerId, "@\(botAddress) \(query)", nil) + } + } + + if let chatTypes { + let peerController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false)) + peerController.peerSelected = { [weak peerController] peer, _ in + completion() + peerController?.dismiss() + activateSwitchInline(peer) + } + if let controller { + controller.push(peerController) } else { - activateSwitchInline() + ((context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface)?.viewControllers.last as? ViewController)?.push(peerController) } + } else { + activateSwitchInline(nil) + } } private static func botOpenPeer(context: AccountContext, peerId: EnginePeer.Id, navigation: ChatControllerInteractionNavigateToPeer, navigationController: NavigationController) { @@ -547,6 +555,10 @@ public extension ChatControllerImpl { if let self { self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil) } + }, openTerms: { [weak self] in + if let self { + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: self.presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: self.presentationData, navigationController: self.effectiveNavigationController, dismissInput: {}) + } }) self.present(controller, in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/Chat/PeerMessageSelectedReactions.swift b/submodules/TelegramUI/Sources/Chat/PeerMessageSelectedReactions.swift index 138e33de1cb..7a30ad19f8e 100644 --- a/submodules/TelegramUI/Sources/Chat/PeerMessageSelectedReactions.swift +++ b/submodules/TelegramUI/Sources/Chat/PeerMessageSelectedReactions.swift @@ -17,9 +17,12 @@ func peerMessageSelectedReactions(context: AccountContext, message: Message) -> if !reaction.isSelected { continue } + if case .stars = reaction.value { + continue + } reactions.insert(reaction.value) switch reaction.value { - case .builtin: + case .builtin, .stars: if let availableReaction = availableReactions?.reactions.first(where: { $0.value == reaction.value }) { result.insert(availableReaction.selectAnimation.fileId) } diff --git a/submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift b/submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift index b1cfa2f40bc..aab9ba81e95 100644 --- a/submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift +++ b/submodules/TelegramUI/Sources/ChatAgeRestrictionAlertController.swift @@ -128,7 +128,7 @@ private final class ChatAgeRestrictionAlertContentNode: AlertContentNode { self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.alwaysLabelNode.attributedText = formattedText("Always show 18+ media", color: theme.primaryColor) + self.alwaysLabelNode.attributedText = formattedText(self.strings.SensitiveContent_ShowAlways, color: theme.primaryColor) self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { @@ -269,10 +269,9 @@ public func chatAgeRestrictionAlertController(context: AccountContext, updatedPr } let strings = presentationData.strings - //TODO:localize var dismissImpl: ((Bool) -> Void)? var getContentNodeImpl: (() -> ChatAgeRestrictionAlertContentNode?)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "View Anyway", action: { + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: strings.SensitiveContent_ViewAnyway, action: { if let alwaysShow = getContentNodeImpl?()?.alwaysShow { completion(alwaysShow) } else { @@ -283,8 +282,8 @@ public func chatAgeRestrictionAlertController(context: AccountContext, updatedPr dismissImpl?(true) })] - let title = "18+ Content" - let text = "This media may contain sensitive content suitable only for adults.\nDo you still want to view it?" + let title = strings.SensitiveContent_Title + let text = strings.SensitiveContent_Text let contentNode = ChatAgeRestrictionAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions) getContentNodeImpl = { [weak contentNode] in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8ecdaba0fa7..7adab2e28e7 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -305,6 +305,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let galleryHiddenMesageAndMediaDisposable = MetaDisposable() let temporaryHiddenGalleryMediaDisposable = MetaDisposable() + + let galleryPresentationContext = PresentationContext() let chatBackgroundNode: WallpaperBackgroundNode public private(set) var controllerInteraction: ChatControllerInteraction? @@ -624,6 +626,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var messageComposeController: MFMessageComposeViewController? + weak var currentSendStarsUndoController: UndoOverlayController? + var currentSendStarsUndoMessageId: EngineMessage.Id? + var currentSendStarsUndoCount: Int = 0 + public var alwaysShowSearchResultsAsList: Bool = false { didSet { self.presentationInterfaceState = self.presentationInterfaceState.updatedDisplayHistoryFilterAsList(self.alwaysShowSearchResultsAsList) @@ -1289,8 +1295,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: { self?.chatDisplayNode.dismissInput() - }, present: { c, a in - self?.present(c, in: .window(.root), with: a, blockInteraction: true) + }, present: { c, a, i in + if case .current = i { + c.presentationArguments = a + c.statusBar.alphaUpdated = { [weak self] transition in + guard let self else { + return + } + self.updateStatusBarPresentation(animated: transition.isAnimated) + } + self?.galleryPresentationContext.present(c, on: PresentationSurfaceLevel(rawValue: 0), blockInteraction: true, completion: {}) + } else { + self?.present(c, in: .window(.root), with: a, blockInteraction: true) + } }, transitionNode: { messageId, media, adjustRect in var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? if let strongSelf = self { @@ -1385,6 +1402,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.controllerInteraction?.sendBotCommand(nil, command) } + }, openAd: { [weak self] messageId in + if let strongSelf = self { + strongSelf.controllerInteraction?.activateAdAction(messageId, nil) + } }, addContact: { [weak self] phoneNumber in if let strongSelf = self { strongSelf.controllerInteraction?.addContact(phoneNumber) @@ -1530,6 +1551,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chosenReaction = .builtin(value) case let .custom(fileId): chosenReaction = .custom(fileId) + case .stars: + chosenReaction = .stars } case let .reaction(value): switch value { @@ -1537,6 +1560,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chosenReaction = .builtin(value) case let .custom(fileId): chosenReaction = .custom(fileId) + case .stars: + chosenReaction = .stars } } @@ -1560,7 +1585,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message) - |> deliverOnMainQueue).startStandalone(next: { allowedReactions in + |> deliverOnMainQueue).startStandalone(next: { allowedReactions, _ in guard let strongSelf = self else { return } @@ -1584,6 +1609,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chosenReaction = .builtin(value) case let .custom(fileId): chosenReaction = .custom(fileId) + case .stars: + chosenReaction = .stars } case let .reaction(value): switch value { @@ -1591,6 +1618,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G chosenReaction = .builtin(value) case let .custom(fileId): chosenReaction = .custom(fileId) + case .stars: + chosenReaction = .stars } } @@ -1598,49 +1627,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var removedReaction: MessageReaction.Reaction? - var messageAlreadyHasThisReaction = false - - let currentReactions = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: context.account.peerId))?.reactions ?? [] - var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) - - if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) { - removedReaction = chosenReaction - updatedReactions.remove(at: index) - } else { - updatedReactions.append(chosenReaction) - messageAlreadyHasThisReaction = currentReactions.contains(where: { $0.value == chosenReaction }) - } - - if removedReaction == nil { - // MARK: Nicegram HideReactions, account added - if !canAddMessageReactions(message: message, account: context.account) { - itemNode.openMessageContextMenu() - return - } - - if strongSelf.context.sharedContext.immediateExperimentalUISettings.disableQuickReaction { - itemNode.openMessageContextMenu() - return - } - - guard let allowedReactions = allowedReactions else { - itemNode.openMessageContextMenu() - return - } - - switch allowedReactions { - case let .set(set): - if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { - itemNode.openMessageContextMenu() - return - } - case .all: - break - } - } - - if removedReaction == nil && !updatedReactions.isEmpty { + if case .stars = chosenReaction { if strongSelf.selectPollOptionFeedback == nil { strongSelf.selectPollOptionFeedback = HapticFeedback() } @@ -1654,7 +1641,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var reactionItem: ReactionItem? switch chosenReaction { - case .builtin: + case .builtin, .stars: for reaction in availableReactions.reactions { guard let centerAnimation = reaction.centerAnimation else { continue @@ -1715,6 +1702,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) }, + onHit: { [weak itemNode] in + guard let strongSelf = self else { + return + } + if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { + strongSelf.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: strongSelf.chatDisplayNode.view)) + } + }, completion: { [weak standaloneReactionAnimation] in standaloneReactionAnimation?.removeFromSupernode() } @@ -1722,93 +1717,229 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }) - } else { - strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode) - if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message, isPremium: strongSelf.presentationInterfaceState.isPremium, forceInline: false) { - var hideRemovedReaction: Bool = false - if let reactions = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) { - for reaction in reactions.reactions { - if reaction.value == removedReaction { - hideRemovedReaction = reaction.count == 1 - break + guard let starsContext = strongSelf.context.starsContext else { + return + } + let _ = (starsContext.state + |> take(1) + |> deliverOnMainQueue).start(next: { [weak strongSelf] state in + guard let strongSelf, let balance = state?.balance else { + return + } + + if balance < 1 { + let _ = (strongSelf.context.engine.payments.starsTopUpOptions() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] options in + guard let strongSelf, let peerId = strongSelf.chatLocation.peerId else { + return } - } + guard let starsContext = strongSelf.context.starsContext else { + return + } + + let purchaseScreen = strongSelf.context.sharedContext.makeStarsPurchaseScreen(context: strongSelf.context, starsContext: starsContext, options: options, purpose: .transfer(peerId: peerId, requiredStars: 1), completion: { result in + let _ = result + //TODO:release + }) + strongSelf.push(purchaseScreen) + }) + + return } - let standaloneDismissAnimation = StandaloneDismissReactionAnimation() - standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) - standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, isIncoming: message.effectivelyIncoming(strongSelf.context.account.peerId), completion: { [weak standaloneDismissAnimation] in - standaloneDismissAnimation?.removeFromSupernode() - }) - } - } - - let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in - switch reaction { - case let .builtin(value): - return .builtin(value) - case let .custom(fileId): - return .custom(fileId: fileId, file: nil) + strongSelf.context.engine.messages.sendStarsReaction(id: message.id, count: 1, isAnonymous: false) + strongSelf.displayOrUpdateSendStarsUndo(messageId: message.id, count: 1) + }) + } else { + var removedReaction: MessageReaction.Reaction? + var messageAlreadyHasThisReaction = false + + let currentReactions = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: context.account.peerId))?.reactions ?? [] + var updatedReactions: [MessageReaction.Reaction] = currentReactions.filter(\.isSelected).map(\.value) + + if let index = updatedReactions.firstIndex(where: { $0 == chosenReaction }) { + removedReaction = chosenReaction + updatedReactions.remove(at: index) + } else { + updatedReactions.append(chosenReaction) + messageAlreadyHasThisReaction = currentReactions.contains(where: { $0.value == chosenReaction }) } - } - - if !strongSelf.presentationInterfaceState.isPremium && mappedUpdatedReactions.count > strongSelf.context.userLimits.maxReactionsPerMessage { - let _ = (ApplicationSpecificNotice.incrementMultipleReactionsSuggestion(accountManager: strongSelf.context.sharedContext.accountManager) - |> deliverOnMainQueue).startStandalone(next: { [weak self] count in - guard let self else { + + if removedReaction == nil { + // MARK: Nicegram HideReactions, account added + if !canAddMessageReactions(message: message, account: context.account) { + itemNode.openMessageContextMenu() return } - if count < 1 { - let context = self.context - let controller = UndoOverlayController( - presentationData: self.presentationData, - content: .premiumPaywall(title: nil, text: self.presentationData.strings.Chat_Reactions_MultiplePremiumTooltip, customUndoText: nil, timeout: nil, linkAction: nil), - elevatedLayout: false, - action: { [weak self] action in - if case .info = action { - if let self { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .reactions, forceDark: false, dismissed: nil) - self.push(controller) + + if strongSelf.context.sharedContext.immediateExperimentalUISettings.disableQuickReaction { + itemNode.openMessageContextMenu() + return + } + + guard let allowedReactions = allowedReactions else { + itemNode.openMessageContextMenu() + return + } + + switch allowedReactions { + case let .set(set): + if !messageAlreadyHasThisReaction && updatedReactions.contains(where: { !set.contains($0) }) { + itemNode.openMessageContextMenu() + return + } + case .all: + break + } + } + + if removedReaction == nil && !updatedReactions.isEmpty { + if strongSelf.selectPollOptionFeedback == nil { + strongSelf.selectPollOptionFeedback = HapticFeedback() + } + strongSelf.selectPollOptionFeedback?.tap() + + itemNode.awaitingAppliedReaction = (chosenReaction, { [weak itemNode] in + guard let strongSelf = self else { + return + } + if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: chosenReaction) { + var reactionItem: ReactionItem? + + switch chosenReaction { + case .builtin, .stars: + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if reaction.value == chosenReaction { + reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + ) + break } } - return true + case let .custom(fileId): + if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile { + reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: chosenReaction), + appearAnimation: itemFile, + stillAnimation: itemFile, + listAnimation: itemFile, + largeListAnimation: itemFile, + applicationAnimation: nil, + largeApplicationAnimation: nil, + isCustom: true + ) + } } - ) - self.present(controller, in: .current) + + if let reactionItem = reactionItem { + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: strongSelf.chatDisplayNode.historyNode.takeGenericReactionEffect()) + + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.presentationData.theme, + animationCache: strongSelf.controllerInteraction!.presentationContext.animationCache, + reaction: reactionItem, + avatarPeers: [], + playHaptic: false, + isLarge: false, + targetView: targetView, + addStandaloneReactionAnimation: { standaloneReactionAnimation in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + } + } + }) + } else { + strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode) + + if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message, isPremium: strongSelf.presentationInterfaceState.isPremium, forceInline: false) { + var hideRemovedReaction: Bool = false + if let reactions = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) { + for reaction in reactions.reactions { + if reaction.value == removedReaction { + hideRemovedReaction = reaction.count == 1 + break + } + } + } + + let standaloneDismissAnimation = StandaloneDismissReactionAnimation() + standaloneDismissAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneDismissAnimation) + standaloneDismissAnimation.animateReactionDismiss(sourceView: targetView, hideNode: hideRemovedReaction, isIncoming: message.effectivelyIncoming(strongSelf.context.account.peerId), completion: { [weak standaloneDismissAnimation] in + standaloneDismissAnimation?.removeFromSupernode() + }) } - }) - } - - let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: false, storeAsRecentlyUsed: false).startStandalone() - - #if DEBUG - if strongSelf.context.sharedContext.applicationBindings.appBuildType == .internal { - if mappedUpdatedReactions.contains(where: { - if case let .custom(fileId, _) = $0, fileId == MessageReaction.starsReactionId { - return true - } else { - return false + } + + let mappedUpdatedReactions = updatedReactions.map { reaction -> UpdateMessageReaction in + switch reaction { + case let .builtin(value): + return .builtin(value) + case let .custom(fileId): + return .custom(fileId: fileId, file: nil) + case .stars: + return .stars } - }) { - let _ = (strongSelf.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) - |> deliverOnMainQueue).start(next: { [weak strongSelf, weak itemNode] files in - guard let strongSelf, let file = files[MessageReaction.starsReactionId] else { + } + + if !strongSelf.presentationInterfaceState.isPremium && mappedUpdatedReactions.count > strongSelf.context.userLimits.maxReactionsPerMessage { + let _ = (ApplicationSpecificNotice.incrementMultipleReactionsSuggestion(accountManager: strongSelf.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self] count in + guard let self else { return } - //TODO:localize - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .starsSent(context: strongSelf.context, file: file, amount: 1, title: "Star Sent", text: "Long tap on {star} to select custom quantity of stars."), elevatedLayout: false, action: { _ in - return false - }), in: .current) - - if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: chosenReaction) { - strongSelf.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: strongSelf.chatDisplayNode.view)) + if count < 1 { + let context = self.context + let controller = UndoOverlayController( + presentationData: self.presentationData, + content: .premiumPaywall(title: nil, text: self.presentationData.strings.Chat_Reactions_MultiplePremiumTooltip, customUndoText: nil, timeout: nil, linkAction: nil), + elevatedLayout: false, + action: { [weak self] action in + if case .info = action { + if let self { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .reactions, forceDark: false, dismissed: nil) + self.push(controller) + } + } + return true + } + ) + self.present(controller, in: .current) } }) } + + let _ = updateMessageReactionsInteractively(account: strongSelf.context.account, messageIds: [message.id], reactions: mappedUpdatedReactions, isLarge: false, storeAsRecentlyUsed: false).startStandalone() } - #endif } }) }, activateMessagePinch: { [weak self] sourceNode in @@ -2698,9 +2829,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G inputData.get(), starsContext.state ) - |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)? in if let data, let state { - return (state, data.form, data.botPeer) + return (state, data.form, data.botPeer, message.forwardInfo?.sourceMessageId == nil ? message.author : nil) } else { return nil } @@ -2746,9 +2877,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G inputData.get(), starsContext.state ) - |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)? in if let data, let state { - return (state, data.form, data.botPeer) + return (state, data.form, data.botPeer, nil) } else { return nil } @@ -3764,6 +3895,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else { return } + + var progress = progress + if progress == nil { + self.chatDisplayNode.historyNode.forEachVisibleMessageItemNode { itemView in + if itemView.item?.message.id == messageId { + progress = itemView.makeProgress() + } + } + } + self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId) self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true, progress: progress)) }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in @@ -4303,7 +4444,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } if alwaysShow { - self.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: "You can update the visibility of sensitive media in [Data and Storage > Show 18+ Content]().", timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + let _ = updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: true).start() + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: self.presentationData.strings.SensitiveContent_SettingsInfo, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { [weak self] action in + if case .info = action, let self { + let controller = self.context.sharedContext.makeDataAndStorageController(context: self.context, sensitiveContent: true) + self.push(controller) + } + return false + }), in: .current) } reveal() }) @@ -4452,6 +4601,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G text = self.presentationData.strings.Chat_ToastQuoteChatUnavailbalePrivateChat } self.controllerInteraction?.displayUndo(.info(title: nil, text: text, timeout: nil, customUndoText: nil)) + }, forceUpdateWarpContents: { [weak self] in + guard let self else { + return + } + self.chatDisplayNode.forceUpdateWarpContents() }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency @@ -4968,7 +5122,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if peer.id.isReplies { imageOverride = .repliesIcon } else if peer.id.isAnonymousSavedMessages { - imageOverride = .anonymousSavedMessagesIcon + imageOverride = .anonymousSavedMessagesIcon(isColored: true) } else if peer.isDeleted { imageOverride = .deletedIcon } else { @@ -5764,7 +5918,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if savedMessagesPeerId.isReplies { imageOverride = .repliesIcon } else if savedMessagesPeerId.isAnonymousSavedMessages { - imageOverride = .anonymousSavedMessagesIcon + imageOverride = .anonymousSavedMessagesIcon(isColored: true) } else if let peer = savedMessagesPeer?.peer, peer.isDeleted { imageOverride = .deletedIcon } else { @@ -6693,23 +6847,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - func themeAndStringsUpdated() { - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - switch self.presentationInterfaceState.mode { - case let .standard(standardMode): - switch standardMode { - case .embedded: + func updateStatusBarPresentation(animated: Bool = false) { + if !self.galleryPresentationContext.controllers.isEmpty, let statusBarStyle = (self.galleryPresentationContext.controllers.last?.0 as? ViewController)?.statusBar.statusBarStyle { + self.statusBar.updateStatusBarStyle(statusBarStyle, animated: animated) + } else { + switch self.presentationInterfaceState.mode { + case let .standard(standardMode): + switch standardMode { + case .embedded: + self.statusBar.statusBarStyle = .Ignore + default: + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.deferScreenEdgeGestures = [] + } + case .overlay: + self.statusBar.statusBarStyle = .Hide + self.deferScreenEdgeGestures = [.top] + case .inline: self.statusBar.statusBarStyle = .Ignore - default: - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.deferScreenEdgeGestures = [] } - case .overlay: - self.statusBar.statusBarStyle = .Hide - self.deferScreenEdgeGestures = [.top] - case .inline: - self.statusBar.statusBarStyle = .Ignore } + } + + func themeAndStringsUpdated() { + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.updateStatusBarPresentation() self.updateNavigationBarPresentation() self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in var state = state @@ -7089,6 +7251,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G override public func loadDisplayNode() { self.loadDisplayNodeImpl() + self.galleryPresentationContext.view = self.view + self.galleryPresentationContext.controllersUpdated = { [weak self] _ in + guard let self else { + return + } + self.updateStatusBarPresentation() + } } override public func viewWillAppear(_ animated: Bool) { diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index fbad3ba82c1..ef5d3f69b1c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -196,7 +196,7 @@ extension ChatControllerImpl { restrictedBy: self.context.account.peerId, timestamp: 0, isMember: false - ), rank: nil) + ), rank: nil, subscriptionUntilDate: nil) } let peer = author @@ -207,7 +207,7 @@ extension ChatControllerImpl { switch participant { case .creator: break - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) } else { @@ -294,7 +294,7 @@ extension ChatControllerImpl { restrictedBy: self.context.account.peerId, timestamp: 0, isMember: false - ), rank: nil) + ), rank: nil, subscriptionUntilDate: nil) } let _ = (self.context.engine.data.get( @@ -312,7 +312,7 @@ extension ChatControllerImpl { switch participant { case .creator: break - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): if let banInfo { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights) } else { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 25b4655028f..37730be0ca4 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1264,6 +1264,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { return CGSize(width: layout.size.width, height: height) } + func forceUpdateWarpContents() { + guard let (layout, _) = self.validLayout else { + return + } + self.wrappingNode.update(size: layout.size, cornerRadius: layout.deviceMetrics.screenCornerRadius, transition: .immediate) + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool, @escaping () -> Void) -> Void, updateExtraNavigationBarBackgroundHeight: (CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void) { let transition: ContainedViewLayoutTransition if let _ = self.scheduledAnimateInAsOverlayFromNode { @@ -2239,6 +2246,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { childrenLayout.intrinsicInsets = UIEdgeInsets(top: listInsets.top, left: listInsets.left, bottom: listInsets.bottom, right: listInsets.right) } self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition) + self.controller?.galleryPresentationContext.containerLayoutUpdated(layout, transition: transition) listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, scrollIndicatorInsets: listScrollIndicatorInsets, duration: duration, curve: curve, ensureTopInsetForOverlayHighlightedItems: ensureTopInsetForOverlayHighlightedItems), additionalScrollDistance, scrollToTop, { [weak self] in if let strongSelf = self { diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 4f54e76bcf6..310ce9af8ec 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -17,6 +17,9 @@ import SavedTagNameAlertController import PremiumUI import ChatSendStarsScreen import ChatMessageItemCommon +import ChatMessageItemView +import ReactionSelectionNode +import AnimatedTextComponent extension ChatControllerImpl { func presentTagPremiumPaywall() { @@ -41,7 +44,7 @@ extension ChatControllerImpl { let reactionFile: Signal switch value { - case .builtin: + case .builtin, .stars: reactionFile = self.context.engine.stickers.availableReactions() |> take(1) |> map { availableReactions -> TelegramMediaFile? in @@ -161,45 +164,10 @@ extension ChatControllerImpl { self.window?.presentInGlobalOverlay(controller) }) } else { - var debug = false - #if DEBUG - debug = true - #endif - if self.context.sharedContext.applicationBindings.appBuildType == .internal { - debug = true - } - - if debug, case .custom(MessageReaction.starsReactionId) = value { - let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId) - |> deliverOnMainQueue).start(next: { [weak self] initialData in - guard let self, let initialData else { - return - } - self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount in - guard let self else { - return - } - - let _ = (self.context.engine.stickers.resolveInlineStickers(fileIds: [MessageReaction.starsReactionId]) - |> deliverOnMainQueue).start(next: { [weak self] files in - guard let self, let file = files[MessageReaction.starsReactionId] else { - return - } - - //TODO:localize - let title: String - if amount == 1 { - title = "Star Sent" - } else { - title = "\(amount) Stars Sent" - } - - self.present(UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, file: file, amount: amount, title: title, text: nil), elevatedLayout: false, action: { _ in - return false - }), in: .current) - }) - })) - }) + if case .stars = value { + gesture?.cancel() + cancelParentGestures(view: sourceView) + self.openMessageSendStarsScreen(message: message) return } @@ -369,7 +337,7 @@ extension ChatControllerImpl { let reactionFile: TelegramMediaFile? switch value { - case .builtin: + case .builtin, .stars: reactionFile = availableReactions?.reactions.first(where: { $0.value == value })?.selectAnimation case let .custom(fileId): reactionFile = customEmoji[fileId] @@ -399,4 +367,176 @@ extension ChatControllerImpl { }) } } + + func openMessageSendStarsScreen(message: Message) { + if let current = self.currentSendStarsUndoController { + self.currentSendStarsUndoController = nil + current.dismiss() + } + self.context.engine.messages.forceSendPendingSendStarsReaction(id: message.id) + + let reactionsAttribute = mergedMessageReactions(attributes: message.attributes, isTags: false) + let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, messageId: message.id, topPeers: reactionsAttribute?.topPeers ?? []) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self, let initialData else { + return + } + HapticFeedback().tap() + self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, isAnonymous, isBecomingTop, transitionOut in + guard let self, amount > 0 else { + return + } + + var sourceItemNode: ChatMessageItemView? + self.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + if itemNode.item?.message.id == message.id { + sourceItemNode = itemNode + return + } + } + } + + if let itemNode = sourceItemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: .stars) { + var reactionItem: ReactionItem? + + for reaction in availableReactions.reactions { + guard let centerAnimation = reaction.centerAnimation else { + continue + } + guard let aroundAnimation = reaction.aroundAnimation else { + continue + } + if reaction.value == .stars { + reactionItem = ReactionItem( + reaction: ReactionItem.Reaction(rawValue: reaction.value), + appearAnimation: reaction.appearAnimation, + stillAnimation: reaction.selectAnimation, + listAnimation: centerAnimation, + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation, + isCustom: false + ) + break + } + } + + if let reactionItem { + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.chatDisplayNode.historyNode.takeGenericReactionEffect()) + + self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + + self.view.window?.addSubview(standaloneReactionAnimation.view) + standaloneReactionAnimation.frame = self.chatDisplayNode.bounds + standaloneReactionAnimation.animateOutToReaction( + context: self.context, + theme: self.presentationData.theme, + item: reactionItem, + value: .stars, + sourceView: transitionOut.sourceView, + targetView: targetView, + hideNode: false, + forceSwitchToInlineImmediately: false, + animateTargetContainer: nil, + addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = self.chatDisplayNode.bounds + self.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, + onHit: { [weak self, weak itemNode] in + guard let self else { + return + } + + if isBecomingTop { + self.chatDisplayNode.animateQuizCorrectOptionSelected() + } + + if let itemNode, let targetView = itemNode.targetReactionView(value: .stars) { + self.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: self.chatDisplayNode.view)) + } + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.view.removeFromSuperview() + } + ) + } + } + + let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount), isAnonymous: isAnonymous) + self.displayOrUpdateSendStarsUndo(messageId: message.id, count: Int(amount)) + })) + }) + } + + func displayOrUpdateSendStarsUndo(messageId: EngineMessage.Id, count: Int) { + if self.currentSendStarsUndoMessageId != messageId { + if let current = self.currentSendStarsUndoController { + self.currentSendStarsUndoController = nil + current.dismiss() + } + } + + if let _ = self.currentSendStarsUndoController { + self.currentSendStarsUndoCount += count + } else { + self.currentSendStarsUndoCount = count + } + + let title: String = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount)) + + let textItems = extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ + 0: .number(self.currentSendStarsUndoCount, minDigits: 1), + 1: .text(self.presentationData.strings.Chat_ToastStarsSent_TextStarAmount(Int32(self.currentSendStarsUndoCount))) + ]) + + self.currentSendStarsUndoMessageId = messageId + if let current = self.currentSendStarsUndoController { + current.content = .starsSent(context: self.context, title: title, text: textItems) + } else { + let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems), elevatedLayout: false, position: .top, action: { [weak self] action in + guard let self else { + return false + } + if case .undo = action { + self.context.engine.messages.cancelPendingSendStarsReaction(id: messageId) + } + return false + }) + self.currentSendStarsUndoController = controller + self.present(controller, in: .current) + } + } +} + +private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { + var textItems: [AnimatedTextComponent.Item] = [] + + var previousIndex = 0 + let nsString = string.string as NSString + for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { + if range.range.lowerBound > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) + } + if let value = mapping[range.index] { + let isUnbreakable: Bool + switch value { + case .text: + isUnbreakable = true + case .number: + isUnbreakable = false + } + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) + } + previousIndex = range.range.upperBound + } + if nsString.length > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) + } + + return textItems } diff --git a/submodules/TelegramUI/Sources/ChatControllerRemoveAd.swift b/submodules/TelegramUI/Sources/ChatControllerRemoveAd.swift index c8e4710f9de..5d7e3c0f887 100644 --- a/submodules/TelegramUI/Sources/ChatControllerRemoveAd.swift +++ b/submodules/TelegramUI/Sources/ChatControllerRemoveAd.swift @@ -9,7 +9,7 @@ import TelegramPresentationData import PresentationDataUtils import ChatMessageItemView -extension ChatControllerImpl { +public extension ChatControllerImpl { func removeAd(opaqueId: Data) { var foundItemNode: ChatMessageItemView? self.chatDisplayNode.historyNode.forEachItemNode { itemNode in diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index d11043b5ae1..0c8f47b7581 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -87,34 +87,35 @@ func chatHistoryEntriesForView( if (associatedData.subject?.isService ?? false) { } else { - if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn { - joinMessage = Message( - stableId: UInt32.max - 1000, - stableVersion: 0, - id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0), - globallyUniqueId: nil, - groupingKey: nil, - groupInfo: nil, - threadId: nil, - timestamp: invitedOn, - flags: [.Incoming], - tags: [], - globalTags: [], - localTags: [], - customTags: [], - forwardInfo: nil, - author: channelPeer, - text: "", - attributes: [], - media: [TelegramMediaAction(action: .joinedByRequest)], - peers: SimpleDictionary(), - associatedMessages: SimpleDictionary(), - associatedMessageIds: [], - associatedMedia: [:], - associatedThreadInfo: nil, - associatedStories: [:] - ) - } else if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) { +// if case let .peer(peerId) = location, case let cachedData = cachedData as? CachedChannelData, let invitedOn = cachedData?.invitedOn { +// joinMessage = Message( +// stableId: UInt32.max - 1000, +// stableVersion: 0, +// id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0), +// globallyUniqueId: nil, +// groupingKey: nil, +// groupInfo: nil, +// threadId: nil, +// timestamp: invitedOn, +// flags: [.Incoming], +// tags: [], +// globalTags: [], +// localTags: [], +// customTags: [], +// forwardInfo: nil, +// author: channelPeer, +// text: "", +// attributes: [], +// media: [TelegramMediaAction(action: .joinedByRequest)], +// peers: SimpleDictionary(), +// associatedMessages: SimpleDictionary(), +// associatedMessageIds: [], +// associatedMedia: [:], +// associatedThreadInfo: nil, +// associatedStories: [:] +// ) +// } else + if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) { joinMessage = Message( stableId: UInt32.max - 1000, stableVersion: 0, diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 76d9b566168..3682a6c75ee 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -371,7 +371,8 @@ private func extractAssociatedData( audioTranscriptionTrial: AudioTranscription.TrialState, chatThemes: [TelegramTheme], deviceContactsNumbers: Set, - isInline: Bool + isInline: Bool, + showSensitiveContent: Bool ) -> ChatMessageItemAssociatedData { var automaticDownloadPeerId: EnginePeer.Id? var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel @@ -426,7 +427,7 @@ private func extractAssociatedData( automaticDownloadPeerId = message.peerId } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent) } private extension ChatHistoryLocationInput { @@ -1587,6 +1588,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto self.allAdMessagesPromise.get() ) + let contentSettings = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ContentSettings()) + let maxReadStoryId: Signal if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudUser { maxReadStoryId = self.context.account.postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .peer(peerId))]) @@ -1662,8 +1665,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto recommendedChannels, audioTranscriptionTrial, chatThemes, - deviceContactsNumbers - ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers in + deviceContactsNumbers, + contentSettings + ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises func applyHole() { @@ -1883,7 +1887,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto translateToLanguage = languageCode } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive")) var includeEmbeddedSavedChatInfo = false if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated { @@ -4063,7 +4067,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto var reactionItem: ReactionItem? switch updatedReaction { - case .builtin: + case .builtin, .stars: if let availableReactions = item.associatedData.availableReactions { for reaction in availableReactions.reactions { guard let centerAnimation = reaction.centerAnimation else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 9d582897804..90acce94fbc 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -615,7 +615,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { _, f in f(.dismissWithoutContent) - controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context)) + controllerInteraction.navigationController()?.pushViewController(AdInfoScreen(context: context, forceDark: true)) }))) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) @@ -1804,26 +1804,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState }))) } } -// if message.id.peerId.isGroupOrChannel { -// //TODO:localize -// if message.isAgeRestricted() { -// actions.append(.action(ContextMenuActionItem(text: "Unmark as 18+", icon: { theme in -// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AgeUnmark"), color: theme.actionSheet.primaryTextColor) -// }, action: { c, _ in -// c?.dismiss(completion: { -// controllerInteraction.openMessageStats(messages[0].id) -// }) -// }))) -// } else { -// actions.append(.action(ContextMenuActionItem(text: "Mark as 18+", icon: { theme in -// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AgeMark"), color: theme.actionSheet.primaryTextColor) -// }, action: { c, _ in -// c?.dismiss(completion: { -// controllerInteraction.openMessageStats(messages[0].id) -// }) -// }))) -// } -// } if isReplyThreadHead { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ViewInChannel, icon: { theme in @@ -2036,7 +2016,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if peer is TelegramChannel { participantSignal = context.engine.peers.fetchChannelParticipant(peerId: peer.id, participantId: user.id) } else if peer is TelegramGroup { - participantSignal = .single(.member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil)) + participantSignal = .single(.member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil)) } else { participantSignal = .single(nil) } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index b339d07840c..ebf77e9191a 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -290,6 +290,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in + }, openStarsTopup: { _ in }, dismissNotice: { _ in }, editPeer: { _ in }) diff --git a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift index 1dbeb89377e..fae6aca6542 100644 --- a/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchTitleAccessoryPanelNode.swift @@ -484,6 +484,8 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat break case let .custom(fileId): customFileIds.append(fileId) + case .stars: + break } } @@ -510,7 +512,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat let title = savedMessageTags?.tags.first(where: { $0.reaction == reaction })?.title switch reaction { - case .builtin: + case .builtin, .stars: if let availableReactions { inner: for availableReaction in availableReactions.reactions { if availableReaction.value == reaction { @@ -763,7 +765,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, Chat let reactionFile: Signal switch reaction { - case .builtin: + case .builtin, .stars: reactionFile = self.context.engine.stickers.availableReactions() |> take(1) |> map { availableReactions -> TelegramMediaFile? in diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 88d69874baa..e5741bad8a7 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -172,6 +172,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { }, openStories: { _, _ in }, + openStarsTopup: { _ in + }, dismissNotice: { _ in }, editPeer: { _ in diff --git a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift index 8d556fb8886..d0eab604ecb 100644 --- a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift +++ b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift @@ -387,6 +387,8 @@ final class ManagedAudioRecorderContext { return } + let _ = AudioUnitSetProperty(audioUnit, kAUVoiceIOProperty_MuteOutput, kAudioUnitScope_Global, 0, &zero, 4) + guard AudioUnitInitialize(audioUnit) == noErr else { AudioComponentInstanceDispose(audioUnit) return diff --git a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift index 49f995ab328..eef3f6a2b40 100644 --- a/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift +++ b/submodules/TelegramUI/Sources/Nicegram/NGDeeplinkHandler.swift @@ -1,3 +1,4 @@ +import FeatAssistant import Foundation import AccountContext import Display @@ -9,7 +10,6 @@ import FeatRewardsUI import FeatTasks import NGAiChatUI import NGAnalytics -import NGAssistantUI import FeatAuth import NGCore import class NGCoreUI.SharedLoadingView @@ -100,7 +100,7 @@ class NGDeeplinkHandler { case "refferaldraw": if #available(iOS 15.0, *) { Task { @MainActor in - AssistantUITgHelper.showReferralDrawFromDeeplink() + AssistantTgHelper.showReferralDrawFromDeeplink() } return true } else { @@ -174,7 +174,7 @@ private extension NGDeeplinkHandler { func handleAssistant(url: URL) -> Bool { if #available(iOS 15.0, *) { Task { @MainActor in - AssistantUITgHelper.routeToAssistant( + AssistantTgHelper.routeToAssistant( source: .deeplink ) } diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index cade6b33775..4ad460d22ec 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -164,7 +164,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: params.addToTransitionSurface) } return nil - })) + }), .window(.root)) return true case .map: params.dismissInput() @@ -221,32 +221,44 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } }, getSourceRect: params.getSourceRect) params.dismissInput() - params.present(controller, nil) + params.present(controller, nil, .window(.root)) return true case let .document(file, immediateShare): params.dismissInput() let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } if immediateShare { let controller = ShareController(context: params.context, subject: .media(.standalone(media: file)), immediateExternalShare: true) - params.present(controller, nil) + params.present(controller, nil, .window(.root)) } else if let rootController = params.navigationController?.view.window?.rootViewController { let proceed = { - if params.context.sharedContext.immediateExperimentalUISettings.browserExperiment && BrowserScreen.supportedDocumentMimeTypes.contains(file.mimeType) { + let canShare = !params.message.isCopyProtected() + var useBrowserScreen = false + if BrowserScreen.supportedDocumentMimeTypes.contains(file.mimeType) { + useBrowserScreen = true + } else if let fileName = file.fileName as? NSString, BrowserScreen.supportedDocumentExtensions.contains(fileName.pathExtension.lowercased()) { + useBrowserScreen = true + } + if useBrowserScreen { let subject: BrowserScreen.Subject if file.mimeType == "application/pdf" { - subject = .pdfDocument(file: file) + subject = .pdfDocument(file: file, canShare: canShare) } else { - subject = .document(file: file) + subject = .document(file: file, canShare: canShare) } let controller = BrowserScreen(context: params.context, subject: subject) + controller.openDocument = { file, canShare in + controller.dismiss() + + presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: canShare) + } params.navigationController?.pushViewController(controller) } else { - presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: !params.message.isCopyProtected()) + presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: canShare) } } if file.mimeType.contains("image/svg") { let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } - params.present(textAlertController(context: params.context, title: nil, text: presentationData.strings.OpenFile_PotentiallyDangerousContentAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.OpenFile_Proceed, action: { proceed() })] ), nil) + params.present(textAlertController(context: params.context, title: nil, text: presentationData.strings.OpenFile_PotentiallyDangerousContentAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.OpenFile_Proceed, action: { proceed() })] ), nil, .window(.root)) } else { proceed() } @@ -308,7 +320,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: params.addToTransitionSurface) } return nil - })) + }), params.message.adAttribute != nil ? .current : .window(.root)) }) return true case let .secretGallery(gallery): @@ -319,7 +331,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: params.addToTransitionSurface) } return nil - })) + }), .window(.root)) return true case let .other(otherMedia): params.dismissInput() @@ -362,7 +374,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { return GalleryTransitionArguments(transitionNode: selectedTransitionNode, addToTransitionSurface: params.addToTransitionSurface) } return nil - })) + }), .window(.root)) case let .theme(media): params.dismissInput() let path = params.context.account.postbox.mediaBox.completedResourcePath(media.resource) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index c92885e83d2..980473852cb 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -283,6 +283,56 @@ func openResolvedUrlImpl( openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) case let .peek(peer, deadline): openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: ChatPeekTimeout(deadline: deadline, linkData: link))) + case let .invite(invite): + if let subscriptionPricing = invite.subscriptionPricing, let subscriptionFormId = invite.subscriptionFormId, let starsContext = context.starsContext { + let inputData = Promise() + var photo: [TelegramMediaImageRepresentation] = [] + if let photoRepresentation = invite.photoRepresentation { + photo.append(photoRepresentation) + } + let channel = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), accessHash: .genericPublic(0), title: invite.title, username: nil, photo: photo, creationDate: 0, version: 0, participationStatus: .left, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: invite.nameColor, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil) + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: subscriptionPricing.amount, startParam: "", extendedMedia: nil, flags: [], version: 0) + + inputData.set(.single(BotCheckoutController.InputData( + form: BotPaymentForm( + id: subscriptionFormId, + canSaveCredentials: false, + passwordMissing: false, + invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: subscriptionPricing.amount)], tip: nil, termsInfo: nil), + paymentBotId: channel.id, + providerId: nil, + url: nil, + nativeProvider: nil, + savedInfo: nil, + savedCredentials: [], + additionalPaymentMethods: [] + ), + validatedFormInfo: nil, + botPeer: EnginePeer(channel) + ))) + + let starsInputData = combineLatest( + inputData.get(), + starsContext.state + ) + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)? in + if let data, let state { + return (state, data.form, data.botPeer, nil) + } else { + return nil + } + } + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = context.sharedContext.makeStarsSubscriptionTransferScreen(context: context, starsContext: starsContext, invoice: invoice, link: link, inputData: starsInputData, navigateToPeer: { peer in + openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) + }) + navigationController?.pushViewController(controller) + }) + } else { + present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in + openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) + }, parentNavigationController: navigationController, resolvedState: resolvedState), nil) + } default: present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) @@ -656,12 +706,38 @@ func openResolvedUrlImpl( if let navigationController = navigationController { navigationController.pushViewController(controller, animated: true) } - case let .starsTopup(amount): + case let .starsTopup(amount, purpose): dismissInput() if let starsContext = context.starsContext { - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .generic(requiredStars: amount), completion: { _ in }) - if let navigationController = navigationController { - navigationController.pushViewController(controller, animated: true) + let proceed = { + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .topUp(requiredStars: amount, purpose: purpose), completion: { _ in }) + if let navigationController = navigationController { + navigationController.pushViewController(controller, animated: true) + } + } + if let currentState = starsContext.currentState, currentState.balance >= amount { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsBuy", + scale: 0.066, + colors: [:], + title: nil, + text: "You have enough stars at the moment.", + customUndoText: "Buy Anyway", + timeout: nil + ), + elevatedLayout: true, + action: { action in + if case .undo = action { + proceed() + } + return true + }) + present(controller, nil) + } else { + proceed() } } case let .joinVoiceChat(peerId, invite): @@ -840,9 +916,9 @@ func openResolvedUrlImpl( inputData.get(), starsContext.state ) - |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)? in if let data, let state { - return (state, data.form, data.botPeer) + return (state, data.form, data.botPeer, nil) } else { return nil } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index e53e697f759..63fae34c4a8 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -976,18 +976,23 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur handleResolvedUrl(.premiumMultiGift(reference: reference)) } else if parsedUrl.host == "stars_topup" { var amount: Int64? + var purpose: String? if let components = URLComponents(string: "/?" + query) { if let queryItems = components.queryItems { for queryItem in queryItems { if let value = queryItem.value { - if queryItem.name == "amount" { + if queryItem.name == "balance" { amount = Int64(value) + } else if queryItem.name == "purpose" { + purpose = value } } } } } - handleResolvedUrl(.starsTopup(amount: amount)) + if let amount { + handleResolvedUrl(.starsTopup(amount: amount, purpose: purpose)) + } } else if parsedUrl.host == "addlist" { if let components = URLComponents(string: "/?" + query) { var slug: String? diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index c731e46943b..8d9f42b0480 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -184,6 +184,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() @@ -310,7 +311,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu if let location = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .custom(messages, _, loadMore) = location { playlistLocation = .custom(messages: messages, at: id, loadMore: loadMore) } - return strongSelf.context.sharedContext.openChatMessage(OpenChatMessageParams(context: strongSelf.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: playlistLocation)) + return strongSelf.context.sharedContext.openChatMessage(OpenChatMessageParams(context: strongSelf.context, chatLocation: nil, chatFilterTag: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: playlistLocation)) } return false } diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index 071e7646384..ec90f3fd672 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -223,7 +223,7 @@ private enum PollResultsEntry: ItemListNodeEntry { return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .solutionText(text, entities): let _ = entities - //TODO:localize + //TODO:release return ItemListMultilineTextItem(presentationData: presentationData, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks) case let .optionPeer(optionId, _, peer, optionText, optionTextEntities, optionAdditionalText, optionCount, optionExpanded, opaqueIdentifier, shimmeringAlternation, isFirstInOption): let font = Font.regular(13.0) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index aa271317b37..4771ccfa9da 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1781,8 +1781,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return nil } - public func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController { - return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId) + public func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController { + return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId, starsState: starsState) } public func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) { @@ -1918,6 +1918,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, scrollToMessageId: { _ in }, navigateToStory: { _, _ in }, attemptedNavigationToPrivateQuote: { _ in + }, forceUpdateWarpContents: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: backgroundNode as? WallpaperBackgroundNode)) @@ -2286,7 +2287,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } return controller default: - return PremiumDemoScreen(context: context, subject: mappedSubject, action: action) + return PremiumDemoScreen(context: context, subject: mappedSubject, forceDark: forceDark, action: action) } } @@ -2845,6 +2846,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return proxySettingsController(accountManager: sharedContext.accountManager, sharedContext: sharedContext, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData) } + public func makeDataAndStorageController(context: AccountContext, sensitiveContent: Bool) -> ViewController { + return dataAndStorageController(context: context, focusOnItemTag: sensitiveContent ? DataAndStorageEntryTag.sensitiveContent : nil) + } + public func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController { return installedStickerPacksController(context: context, mode: mode, forceTheme: forceTheme) } @@ -2869,16 +2874,28 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, completion: completion) } - public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController { + public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController { return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion) } + public func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController { + return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .starsChatSubscription(hash: link), extendedMedia: [], inputData: inputData, navigateToPeer: navigateToPeer, completion: { _ in }) + } + public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController { - return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer), action: {}) + return StarsTransactionScreen(context: context, subject: .transaction(transaction, peer)) } public func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController { - return StarsTransactionScreen(context: context, subject: .receipt(receipt), action: {}) + return StarsTransactionScreen(context: context, subject: .receipt(receipt)) + } + + public func makeStarsSubscriptionScreen(context: AccountContext, subscription: StarsContext.State.Subscription, update: @escaping (Bool) -> Void) -> ViewController { + return StarsTransactionScreen(context: context, subject: .subscription(subscription), updateSubscription: update) + } + + public func makeStarsSubscriptionScreen(context: AccountContext, peer: EnginePeer, pricing: StarsSubscriptionPricing, importer: PeerInvitationImportersState.Importer, usdRate: Double) -> ViewController { + return StarsTransactionScreen(context: context, subject: .importer(peer, pricing, importer, usdRate)) } public func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController { @@ -2894,7 +2911,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { - return StarsTransactionScreen(context: context, subject: .gift(message), action: {}) + return StarsTransactionScreen(context: context, subject: .gift(message)) } public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index f8d58fd3452..2b2520a909f 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -1,6 +1,6 @@ // MARK: Nicegram Assistant import CoreSwiftUI -import NGAssistantUI +import FeatAssistant import let NGCoreUI.images import var NGCoreUI.strings import NGUI @@ -181,7 +181,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } return !window.hasOverlayController() } - await AssistantUITgHelper.showAlertsFromHomeIfNeeded( + await AssistantTgHelper.showAlertsFromHomeIfNeeded( canPresent: canPresent, showAssitantTooltip: { [weak self] in self?.showNicegramTooltip( @@ -265,7 +265,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon // MARK: Nicegram Assistant if #available(iOS 15.0, *) { let assistantController = NativeControllerWrapper( - controller: AssistantUITgHelper.assistantTab(), + controller: AssistantTgHelper.assistantTab(), accountContext: self.context, adjustSafeArea: true ) @@ -284,7 +284,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon controllers.append(assistantController) } - AssistantUITgHelper.routeToAssistantImpl = { [weak self] source in + AssistantTgHelper.routeToAssistantImpl = { [weak self] source in guard let self, let rootTabController else { return } @@ -294,12 +294,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } if let assistantIndex { - AssistantUITgHelper.assistantSource = source + AssistantTgHelper.assistantSource = source popToRoot(animated: true) rootTabController.selectedIndex = assistantIndex } else { - AssistantUITgHelper.presentAssistantModally( + AssistantTgHelper.presentAssistantModally( source: source ) } @@ -315,7 +315,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } if index == assistantIndex { - AssistantUITgHelper.assistantSource = .tabBar + AssistantTgHelper.assistantSource = .tabBar } } } diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 5c3d6a7f650..d387a59935e 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -92,15 +92,15 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n let sourceLocation = InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group) let browserController = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) (controller.navigationController as? NavigationController)?.pushViewController(browserController, animated: true) - case let .join(link): - controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in - openResolvedPeerImpl(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) - }, parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) - case .boost, .chatFolder: +// case let .join(link): +// controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in +// openResolvedPeerImpl(peer, .chat(textInputState: nil, subject: nil, peekData: peekData)) +// }, parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) + case .boost, .chatFolder, .join: if let navigationController = controller.navigationController as? NavigationController { openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, message: nil, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigateToPeer in openResolvedPeerImpl(peer, navigateToPeer) - }, sendFile: nil, sendSticker: nil, sendEmoji: nil, joinVoiceChat: nil, present: { c, a in }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) + }, sendFile: nil, sendSticker: nil, sendEmoji: nil, joinVoiceChat: nil, present: { c, a in }, dismissInput: {}, contentContext: nil, progress: Promise(), completion: nil) } default: break diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift index 6d8b21210ab..581a55b2880 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift @@ -18,7 +18,7 @@ public extension ChannelParticipant { switch self { case .creator: return nil - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): return adminInfo } } @@ -27,7 +27,7 @@ public extension ChannelParticipant { switch self { case .creator: return nil - case let .member(_, _, _, banInfo, _): + case let .member(_, _, _, banInfo, _, _): return banInfo } } @@ -36,7 +36,7 @@ public extension ChannelParticipant { switch self { case .creator: return false - case let .member(_, _, adminInfo, _, _): + case let .member(_, _, adminInfo, _, _, _): if let adminInfo = adminInfo { if adminInfo.promotedBy != peerId { return false @@ -92,7 +92,7 @@ private extension CachedChannelAdminRank { } else { self = .owner } - case let .member(_, _, _, _, rank): + case let .member(_, _, _, _, rank, _): if let rank = rank { self = .custom(rank) } else { @@ -366,7 +366,7 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor switch self.category { case let .admins(query): if let updated = updated, (query == nil || updated.peer.indexName.matchesByTokens(query!)) { - if case let .member(_, _, adminInfo, _, _) = updated.participant, adminInfo == nil { + if case let .member(_, _, adminInfo, _, _, _) = updated.participant, adminInfo == nil { loop: for i in 0 ..< list.count { if list[i].peer.id == updated.peer.id { list.remove(at: i) diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index 005634bca06..4c25ab81ff4 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -321,7 +321,7 @@ public final class PeerChannelMemberCategoriesContextsManager { self.impl.with { impl in for (contextPeerId, context) in impl.contexts { if contextPeerId == peerId { - context.replayUpdates([(.member(id: memberId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), nil, nil)]) + context.replayUpdates([(.member(id: memberId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), nil, nil)]) } } } diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h index 96934adf5ac..6443717eeb1 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h @@ -32,4 +32,6 @@ NSObject * _Nullable makeLuminanceToAlphaFilter(); NSObject * _Nullable makeMonochromeFilter(); void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots); +bool getLayerDisableScreenshots(CALayer * _Nonnull layer); + void setLayerContentsMaskMode(CALayer * _Nonnull layer, bool maskMode); diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index 714d7032bd5..fe257ac6647 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -1,5 +1,7 @@ #import "UIKitUtils.h" +#import + #import #if TARGET_IPHONE_SIMULATOR @@ -236,6 +238,7 @@ - (NSObject * _Nullable)filterWithName:(NSString * _Nonnull)name; return [(id)NSClassFromString(@"CAFilter") filterWithName:@"colorMonochrome"]; } +static const void *layerDisableScreenshotsKey = &layerDisableScreenshotsKey; void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots) { static UITextField *textField = nil; @@ -264,6 +267,17 @@ void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshot textField.secureTextEntry = false; } [secureView setValue:previousLayer forKey:@"layer"]; + + [layer setAssociatedObject:@(disableScreenshots) forKey:layerDisableScreenshotsKey associationPolicy:NSObjectAssociationPolicyRetain]; +} + +bool getLayerDisableScreenshots(CALayer * _Nonnull layer) { + id result = [layer associatedObjectForKey:layerDisableScreenshotsKey]; + if ([result respondsToSelector:@selector(boolValue)]) { + return [(NSNumber *)result boolValue]; + } else { + return false; + } } void setLayerContentsMaskMode(CALayer * _Nonnull layer, bool maskMode) { diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 1b33b8e165e..17cd23ad5fe 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -30,6 +30,9 @@ swift_library( "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/Components/ComponentDisplayAdapters", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 030e3c3a580..3aa34a34058 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -5,6 +5,7 @@ import TelegramPresentationData import TelegramCore import AccountContext import ComponentFlow +import AnimatedTextComponent public enum UndoOverlayContent { case removedChat(title: String, text: String?) @@ -22,7 +23,7 @@ public enum UndoOverlayContent { case chatRemovedFromFolder(chatTitle: String, folderTitle: String) case messagesUnpinned(title: String, text: String, undo: Bool, isHidden: Bool) case setProximityAlert(title: String, text: String, cancelled: Bool) - case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, text: String, action: String?, duration: Double) + case invitedToVoiceChat(context: AccountContext, peer: EnginePeer, title: String?, text: String, action: String?, duration: Double) case linkCopied(text: String) case banned(text: String) case importedMessage(text: String) @@ -39,7 +40,7 @@ public enum UndoOverlayContent { case copy(text: String) case mediaSaved(text: String) case paymentSent(currencyValue: String, itemTitle: String) - case starsSent(context: AccountContext, file: TelegramMediaFile, amount: Int64, title: String, text: String?) + case starsSent(context: AccountContext, title: String, text: [AnimatedTextComponent.Item]) case inviteRequestSent(title: String, text: String) case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index d9398de8a43..75770ad4aa9 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -20,6 +20,9 @@ import AnimatedAvatarSetNode import ComponentFlow import EmojiStatusComponent import TextNodeWithEntities +import BundleIconComponent +import AnimatedTextComponent +import ComponentDisplayAdapters final class UndoOverlayControllerNode: ViewControllerTracingNode { private let presentationData: PresentationData @@ -42,6 +45,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var emojiStatus: ComponentView? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNodeWithEntities + private var textComponent: ComponentView? + private var animatedTextItems: [AnimatedTextComponent.Item]? private let buttonNode: HighlightTrackingButtonNode private let undoButtonTextNode: ImmediateTextNode private let undoButtonNode: HighlightTrackingButtonNode @@ -84,6 +89,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.timerTextNode.displaysAsynchronously = false self.titleNode = ImmediateTextNode() + self.titleNode.layer.anchorPoint = CGPoint() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 0 @@ -380,32 +386,23 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.attributedText = string displayUndo = false self.originalRemainingSeconds = 5 - case let .starsSent(context, file, _, title, text): + case let .starsSent(_, title, textItems): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil self.animationNode = nil + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + let imageBoundingSize = CGSize(width: 34.0, height: 34.0) let emojiStatus = ComponentView() self.emojiStatus = emojiStatus let _ = emojiStatus.update( transition: .immediate, - component: AnyComponent(EmojiStatusComponent( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - content: .animation( - content: .file(file: file), - size: imageBoundingSize, - placeholderColor: UIColor(white: 1.0, alpha: 0.1), - themeColor: .white, - loopMode: .count(1) - ), - isVisibleForAnimations: true, - useSharedAnimation: false, - action: nil + component: AnyComponent(BundleIconComponent( + name: "Premium/Stars/StarLarge", + tintColor: nil )), environment: {}, containerSize: imageBoundingSize @@ -413,34 +410,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.stickerImageSize = imageBoundingSize - if let text { - let formattedString = text - - let string = NSMutableAttributedString(attributedString: NSAttributedString(string: formattedString, font: Font.regular(14.0), textColor: .white)) - let starRange = (string.string as NSString).range(of: "{star}") - if starRange.location != NSNotFound { - string.replaceCharacters(in: starRange, with: "") - string.insert(NSAttributedString(string: ".", attributes: [ - .font: Font.regular(14.0), - ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: MessageReaction.starsReactionId, file: file, custom: nil) - ]), at: starRange.location) - } - - self.textNode.attributedText = string - self.textNode.arguments = TextNodeWithEntities.Arguments( - context: context, - cache: context.animationCache, - renderer: context.animationRenderer, - placeholderColor: UIColor(white: 1.0, alpha: 0.1), - attemptSynchronous: false - ) - self.textNode.visibility = true - } + self.animatedTextItems = textItems - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) - - displayUndo = false - self.originalRemainingSeconds = 3 + displayUndo = true + self.originalRemainingSeconds = 4.5 isUserInteractionEnabled = true case let .messagesUnpinned(title, text, undo, isHidden): self.avatarNode = nil @@ -652,19 +625,21 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { displayUndo = false self.originalRemainingSeconds = 3 - case let .invitedToVoiceChat(context, peer, text, action, duration): + case let .invitedToVoiceChat(context, peer, title, text, action, duration): self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) self.iconNode = nil self.iconCheckNode = nil self.animationNode = nil self.animatedStickerNode = nil + self.titleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.semibold(14.0), textColor: .white) + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) - self.textNode.attributedText = attributedText + self.textNode.attributedText = attributedText self.avatarNode?.setPeer(context: context, theme: presentationData.theme, peer: peer, overrideImage: nil, emptyColor: presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: true) if let action = action { @@ -1483,6 +1458,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { var undoTextColor = self.presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) + var transition: ContainedViewLayoutTransition = .immediate + switch content { case let .info(title, text, _, _), let .universal(_, _, _, title, text, _, _): let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) @@ -1514,12 +1491,19 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) } self.textNode.attributedText = attributedText + case let .starsSent(_, title, textItems): + self.animatedTextItems = textItems + + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + + self.renewWithCurrentContent() + transition = .animated(duration: 0.1, curve: .easeInOut) default: break } if let validLayout = self.validLayout { - self.containerLayoutUpdated(layout: validLayout, transition: .immediate) + self.containerLayoutUpdated(layout: validLayout, transition: transition) } } @@ -1577,7 +1561,41 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } let titleSize = self.titleNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude)) - let textSize = self.textNode.updateLayout(CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude)) + + let maxTextSize = CGSize(width: buttonMinX - 8.0 - leftInset - layout.safeInsets.left - leftMargin, height: .greatestFiniteMagnitude) + + let textSize: CGSize + if let animatedTextItems = self.animatedTextItems { + let textComponent: ComponentView + if let current = self.textComponent { + textComponent = current + } else { + textComponent = ComponentView() + self.textComponent = textComponent + } + textSize = textComponent.update( + transition: ComponentTransition(transition), + component: AnyComponent(AnimatedTextComponent( + font: Font.regular(14.0), + color: .white, + items: animatedTextItems + )), + environment: {}, + containerSize: maxTextSize + ) + if let textComponentView = textComponent.view { + if textComponentView.superview == nil { + textComponentView.layer.anchorPoint = CGPoint() + self.panelWrapperNode.view.addSubview(textComponentView) + } + } + } else { + if let textComponentView = self.textComponent?.view { + self.textComponent = nil + textComponentView.removeFromSuperview() + } + textSize = self.textNode.updateLayout(maxTextSize) + } if !titleSize.width.isZero { contentHeight += titleSize.height + 1.0 @@ -1628,8 +1646,17 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } let textContentOrigin = floor((contentHeight - textContentHeight) / 2.0) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin), size: titleSize)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin + textOffset), size: textSize)) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin), size: titleSize) + transition.updatePosition(node: self.titleNode, position: titleFrame.origin) + self.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + + let textFrame = CGRect(origin: CGPoint(x: leftInset, y: textContentOrigin + textOffset), size: textSize) + if let textComponentView = self.textComponent?.view { + transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) + textComponentView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } else { + transition.updateFrame(node: self.textNode, frame: textFrame) + } if let iconNode = self.iconNode { let iconSize: CGSize diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index bc5e329a88e..b807153dcd4 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -231,7 +231,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) if let phone = phone, let hash = hash { return .cancelAccountReset(phone: phone, hash: hash) } - } else if peerName == "msg" { + } else if peerName == "msg" || peerName == "share" { var url: String? var text: String? var to: String? @@ -241,7 +241,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String) url = value } else if queryItem.name == "text" { text = value - } else if queryItem.name == "to" { + } else if queryItem.name == "to" && peerName != "share" { to = value } } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 40c4537651e..75e0e027496 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -795,9 +795,9 @@ public final class WebAppController: ViewController, AttachmentContainable { inputData.get(), starsContext.state ) - |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)? in if let data, let state { - return (state, data.form, data.botPeer) + return (state, data.form, data.botPeer, nil) } else { return nil } @@ -2010,10 +2010,11 @@ public final class WebAppController: ViewController, AttachmentContainable { let items = combineLatest(queue: Queue.mainQueue(), context.engine.messages.attachMenuBots(), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.botId)), - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotCommands(id: self.botId)) + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotCommands(id: self.botId)), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotPrivacyPolicyUrl(id: self.botId)) ) |> take(1) - |> map { [weak self] attachMenuBots, botPeer, botCommands -> ContextController.Items in + |> map { [weak self] attachMenuBots, botPeer, botCommands, privacyPolicyUrl -> ContextController.Items in var items: [ContextMenuItem] = [] let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) @@ -2100,28 +2101,29 @@ public final class WebAppController: ViewController, AttachmentContainable { }) }))) - if let botCommands { - for command in botCommands { - if command.text == "privacy" { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_PrivacyPolicy, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Privacy"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, _ in - c?.dismiss(completion: nil) - - guard let self else { - return - } - let _ = enqueueMessages(account: self.context.account, peerId: self.botId, messages: [.message(text: "/privacy", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() - - if let botPeer, let navigationController = self.getNavigationController() { - (self.parentController() as? AttachmentController)?.minimizeIfNeeded() - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(botPeer))) - } - }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_PrivacyPolicy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Privacy"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + guard let self else { + return + } + + (self.parentController() as? AttachmentController)?.minimizeIfNeeded() + if let privacyPolicyUrl { + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: privacyPolicyUrl, forceExternal: false, presentationData: self.presentationData, navigationController: self.getNavigationController(), dismissInput: {}) + } else if let botCommands, botCommands.contains(where: { $0.text == "privacy" }) { + let _ = enqueueMessages(account: self.context.account, peerId: self.botId, messages: [.message(text: "/privacy", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() + + if let botPeer, let navigationController = self.getNavigationController() { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(botPeer))) } + } else { + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: self.presentationData.strings.WebApp_PrivacyPolicy_URL, forceExternal: false, presentationData: self.presentationData, navigationController: self.getNavigationController(), dismissInput: {}) } - } - + }))) + if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(source) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) diff --git a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift index 85550aadf39..534d9776429 100644 --- a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift +++ b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift @@ -16,8 +16,8 @@ import Markdown private let textFont = Font.regular(13.0) private let boldTextFont = Font.semibold(13.0) -private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { - return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment) +private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment) } private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { @@ -44,6 +44,7 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { private var validLayout: CGSize? private let morePressed: () -> Void + private let termsPressed: () -> Void override var dismissOnOutsideTap: Bool { return self.isUserInteractionEnabled @@ -55,13 +56,14 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { } } - init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, title: String, text: String, showMore: Bool, requestWriteAccess: Bool, actions: [TextAlertAction], morePressed: @escaping () -> Void) { + init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, title: String, text: String, showMore: Bool, requestWriteAccess: Bool, actions: [TextAlertAction], morePressed: @escaping () -> Void, termsPressed: @escaping () -> Void) { self.strings = strings self.peer = peer self.title = title self.text = text self.showMore = showMore self.morePressed = morePressed + self.termsPressed = termsPressed self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false @@ -145,6 +147,8 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { super.didLoad() self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:)))) + + self.textNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.termsTap(_:)))) } @objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) { @@ -153,18 +157,22 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { } } + @objc private func termsTap(_ gestureRecognizer: UITapGestureRecognizer) { + self.termsPressed() + } + @objc private func moreButtonPressed() { self.morePressed() } override func updateTheme(_ theme: AlertControllerTheme) { self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) - self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = formattedText(self.text, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center) self.moreButton.setAttributedTitle(NSAttributedString(string: self.strings.WebApp_LaunchMoreInfo, font: Font.regular(13.0), textColor: theme.accentColor), for: .normal) self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.accentColor) - self.allowWriteLabelNode.attributedText = formattedText(strings.WebApp_AddToAttachmentAllowMessages(self.peer.compactDisplayTitle).string, color: theme.primaryColor) + self.allowWriteLabelNode.attributedText = formattedText(strings.WebApp_AddToAttachmentAllowMessages(self.peer.compactDisplayTitle).string, color: theme.primaryColor, linkColor: theme.primaryColor) self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { @@ -313,7 +321,15 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { } } -public func webAppLaunchConfirmationController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, requestWriteAccess: Bool = false, completion: @escaping (Bool) -> Void, showMore: (() -> Void)?) -> AlertController { +public func webAppLaunchConfirmationController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, + peer: EnginePeer, + requestWriteAccess: Bool = false, + completion: @escaping (Bool) -> Void, + showMore: (() -> Void)?, + openTerms: @escaping () -> Void +) -> AlertController { let theme = defaultDarkColorPresentationTheme let presentationData: PresentationData if let updatedPresentationData { @@ -337,11 +353,14 @@ public func webAppLaunchConfirmationController(context: AccountContext, updatedP })] let title = peer.compactDisplayTitle - let text = presentationData.strings.WebApp_LaunchConfirmation + let text = presentationData.strings.WebApp_LaunchTermsConfirmation let contentNode = WebAppLaunchConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peer: peer, title: title, text: text, showMore: showMore != nil, requestWriteAccess: requestWriteAccess, actions: actions, morePressed: { dismissImpl?(true) showMore?() + }, termsPressed: { + dismissImpl?(true) + openTerms() }) getContentNodeImpl = { [weak contentNode] in return contentNode diff --git a/versions.json b/versions.json index e3f58d3077f..d6b332e6f8a 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "1.7.5", + "app": "1.7.6", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0"